feat: use react query for site requests

This commit is contained in:
DIYgod 2022-07-21 22:45:32 +01:00
parent 2f0bc2427f
commit 074611e88d
No known key found for this signature in database
GPG Key ID: D159328F47A80DCA
20 changed files with 302 additions and 210 deletions

View File

@ -7,3 +7,5 @@ NEXT_PUBLIC_APP_NAME=Crosslog
NEXT_PUBLIC_OUR_DOMAIN=localhost:3000
NEXT_PUBLIC_DISCORD_LINK="https://discord.gg/9XscSqJEq4"
NEXT_PUBLIC_GITHUB_LINK="https://github.com/Crossbell-Box/Crosslog"
CACHE_MAX_AGE=600000

View File

@ -25,6 +25,8 @@
"@prisma/client": "^3.14.0",
"@proselog/jwt": "^0.1.1",
"@rainbow-me/rainbowkit": "^0.3.3",
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"@trpc/client": "^9.23.2",
"@trpc/next": "^9.23.2",
"@trpc/react": "^9.23.2",
@ -51,7 +53,6 @@
"react-dom": "^18.1.0",
"react-hook-form": "^7.31.1",
"react-hot-toast": "^2.2.0",
"react-query": "^3.39.0",
"react-sortablejs": "^6.1.1",
"refractor": "^4.7.0",
"rehype-prism-plus": "^1.4.0",
@ -63,7 +64,7 @@
"remark-rehype": "^10.1.0",
"sortablejs": "^1.15.0",
"ua-parser-js": "^1.0.2",
"unidata.js": "^0.6.2",
"unidata.js": "^0.6.3",
"unified": "^10.1.2",
"unist-builder": "^3.0.0",
"unist-util-visit": "^4.1.0",

View File

@ -17,6 +17,8 @@ specifiers:
'@prisma/client': ^3.14.0
'@proselog/jwt': ^0.1.1
'@rainbow-me/rainbowkit': ^0.3.3
'@tanstack/react-query': ^4.0.10
'@tanstack/react-query-devtools': ^4.0.10
'@trpc/client': ^9.23.2
'@trpc/next': ^9.23.2
'@trpc/react': ^9.23.2
@ -66,7 +68,6 @@ specifiers:
react-dom: ^18.1.0
react-hook-form: ^7.31.1
react-hot-toast: ^2.2.0
react-query: ^3.39.0
react-sortablejs: ^6.1.1
refractor: ^4.7.0
rehype-prism-plus: ^1.4.0
@ -80,7 +81,7 @@ specifiers:
tailwindcss: ^3.0.24
typescript: ^4.5.5
ua-parser-js: ^1.0.2
unidata.js: ^0.6.2
unidata.js: ^0.6.3
unified: ^10.1.2
unist-builder: ^3.0.0
unist-util-visit: ^4.1.0
@ -104,9 +105,11 @@ dependencies:
'@prisma/client': 3.15.2_prisma@3.15.2
'@proselog/jwt': 0.1.1
'@rainbow-me/rainbowkit': 0.3.4_ksvpodatviltilqty4m7gcxati
'@tanstack/react-query': 4.0.10_biqbaboplfbrettd7655fr4n2y
'@tanstack/react-query-devtools': 4.0.10_react@18.2.0
'@trpc/client': 9.25.3_@trpc+server@9.25.3
'@trpc/next': 9.25.3_uh6lro2uvcz55p6qikxrpfi5oa
'@trpc/react': 9.25.3_ms3at4dqhndwrpx5xroirjehyy
'@trpc/next': 9.25.3_wdrv5arzz3rco7suaqfslncqi4
'@trpc/react': 9.25.3_5nj7bll4cny4mklqtydexm3u4e
'@trpc/server': 9.25.3
'@web-std/form-data': 3.0.2
async-busboy: 1.1.0
@ -130,7 +133,6 @@ dependencies:
react-dom: 18.2.0_react@18.2.0
react-hook-form: 7.33.0_react@18.2.0
react-hot-toast: 2.2.0_biqbaboplfbrettd7655fr4n2y
react-query: 3.39.1_biqbaboplfbrettd7655fr4n2y
react-sortablejs: 6.1.4_yi7d4rnqh6snv6o2bvvwtjnph4
refractor: 4.7.0
rehype-prism-plus: 1.4.1
@ -142,7 +144,7 @@ dependencies:
remark-rehype: 10.1.0
sortablejs: 1.15.0
ua-parser-js: 1.0.2
unidata.js: 0.6.2
unidata.js: 0.6.3
unified: 10.1.2
unist-builder: 3.0.0
unist-util-visit: 4.1.0
@ -1367,6 +1369,46 @@ packages:
resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
dev: true
/@tanstack/match-sorter-utils/8.1.1:
resolution: {integrity: sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==}
engines: {node: '>=12'}
dependencies:
remove-accents: 0.4.2
dev: false
/@tanstack/query-core/4.0.10:
resolution: {integrity: sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==}
dev: false
/@tanstack/react-query-devtools/4.0.10_react@18.2.0:
resolution: {integrity: sha512-3J7LLYQjfjTI0DbPo0bA+M3l4kdvYSWAqihpeG1u93WVyZj0OEFviUv+4cK7+k2AVgQJAPMZ5xvtewKxOOFVrw==}
dependencies:
'@tanstack/match-sorter-utils': 8.1.1
'@types/use-sync-external-store': 0.0.3
use-sync-external-store: 1.2.0_react@18.2.0
transitivePeerDependencies:
- react
dev: false
/@tanstack/react-query/4.0.10_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@tanstack/query-core': 4.0.10
'@types/use-sync-external-store': 0.0.3
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
use-sync-external-store: 1.2.0_react@18.2.0
dev: false
/@trpc/client/9.25.3_@trpc+server@9.25.3:
resolution: {integrity: sha512-4R+IK3cCDjoOTwHDyu+uN0bF3mYQI34i6aDNU3D/dJJxcA8PX5f8yb++nnR4RT6oTnu3yiR3PFC4KslvBtyi9w==}
peerDependencies:
@ -1376,7 +1418,7 @@ packages:
'@trpc/server': 9.25.3
dev: false
/@trpc/next/9.25.3_uh6lro2uvcz55p6qikxrpfi5oa:
/@trpc/next/9.25.3_wdrv5arzz3rco7suaqfslncqi4:
resolution: {integrity: sha512-eAhPIinDTUiEgiPjChNIcqaPgwn+rJ8HX374fwff/tZXg5OVr3dA7rur2nYyQUWO8LvdiLfUAbTxgtoEB8SPKw==}
peerDependencies:
'@trpc/client': 9.25.3
@ -1389,16 +1431,15 @@ packages:
dependencies:
'@babel/runtime': 7.18.3
'@trpc/client': 9.25.3_@trpc+server@9.25.3
'@trpc/react': 9.25.3_ms3at4dqhndwrpx5xroirjehyy
'@trpc/react': 9.25.3_5nj7bll4cny4mklqtydexm3u4e
'@trpc/server': 9.25.3
next: 12.1.6_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-query: 3.39.1_biqbaboplfbrettd7655fr4n2y
react-ssr-prepass: 1.5.0_react@18.2.0
dev: false
/@trpc/react/9.25.3_ms3at4dqhndwrpx5xroirjehyy:
/@trpc/react/9.25.3_5nj7bll4cny4mklqtydexm3u4e:
resolution: {integrity: sha512-/StrJS7hKPPn88YaXX+qRU50MeLZxBmPC5BLVyhRtwmbzK25Q0MTzBgQLrjoCpJ/HgXYZEbB5ddiw8f+/WnqIA==}
peerDependencies:
'@trpc/client': 9.25.3
@ -1412,7 +1453,6 @@ packages:
'@trpc/server': 9.25.3
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-query: 3.39.1_biqbaboplfbrettd7655fr4n2y
dev: false
/@trpc/server/9.25.3:
@ -4081,7 +4121,7 @@ packages:
resolution: {integrity: sha512-UHHu1HqW22ZhK6C/1Zmjf7mQpOwPwLYZ+xcsOgpzistONU8QqvCop6Od29p/kw1GUVoq2Ihu6ItpKLtlojx4FQ==}
engines: {node: '>=16'}
dependencies:
undici: 5.6.0
undici: 5.8.0
dev: false
/file-entry-cache/6.0.1:
@ -6779,25 +6819,6 @@ packages:
p-defer: 3.0.0
dev: false
/react-query/3.39.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-qYKT1bavdDiQZbngWZyPotlBVzcBjDYEJg5RQLBa++5Ix5jjfbEYJmHSZRZD+USVHUSvl/ey9Hu+QfF1QAK80A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.18.3
broadcast-channel: 3.7.0
match-sorter: 6.3.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react-query/4.0.0-beta.23_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-e6mNBVAYGy0M1OwX0mhRB/lCkOedKeqTUrbPjNCqvm8hQGUsJJobqfHVvTv8o6JJaOO2MFcxKF4vZM+PEKbHZA==}
peerDependencies:
@ -7713,13 +7734,13 @@ packages:
engines: {node: '>=12.18'}
dev: true
/undici/5.6.0:
resolution: {integrity: sha512-mc+8SY1fXubTrdx4CXDkeFFGV8lI3Tq4I/70U1V8Z6g4iscGII0uLO7CPnDt56bXEbvaKwo2T2+VrteWbZiXiQ==}
/undici/5.8.0:
resolution: {integrity: sha512-1F7Vtcez5w/LwH2G2tGnFIihuWUlc58YidwLiCv+jR2Z50x0tNXpRRw7eOIJ+GvqCqIkg9SB7NWAJ/T9TLfv8Q==}
engines: {node: '>=12.18'}
dev: false
/unidata.js/0.6.2:
resolution: {integrity: sha512-5QYGPwrWPDDmjdkxzFD9H/402Cjx1m40+5IUx6h/eLMSCWCu8oXsjANmFE1M7ZK9Fd/9nMWXBhvAB3I6i0ErEA==}
/unidata.js/0.6.3:
resolution: {integrity: sha512-w2vWxsJGIZ+t2CLO9yxBStHbuMijmoaPKw4jBhfrUtVuUjD0MMGIUZ5mVbDbbvaNDKCWli5JqaNZjaSmCCmaYw==}
dependencies:
'@types/lodash': 4.14.182
'@urql/core': 2.5.0_graphql@16.5.0

View File

@ -1,5 +1,5 @@
import { Dialog } from "@headlessui/react"
import { useRef, useState } from "react"
import { useRef, useState, useEffect } from "react"
import ReactAvatarEditor from "react-avatar-editor"
import { getUserContentsUrl } from "~/lib/user-contents"
import { Avatar } from "~/components/ui/Avatar"
@ -7,7 +7,7 @@ import { Button } from "~/components/ui/Button"
import toast from "react-hot-toast"
import createPica from "pica"
import { UploadFile, useUploadFile } from "~/hooks/useUploadFile"
import { updateSite } from "~/models/site.model"
import { useUpdateSite } from "~/queries/site"
const AvatarEditorModal: React.FC<{
isOpen: boolean
@ -17,16 +17,13 @@ const AvatarEditorModal: React.FC<{
uploadFile: UploadFile
}> = ({ isOpen, setIsOpen, image, site, uploadFile }) => {
const editorRef = useRef<ReactAvatarEditor | null>(null)
const [isSaving, setIsSaving] = useState(false)
const updateSite = useUpdateSite()
const cropAndSave = async () => {
if (!editorRef.current) return
try {
setIsSaving(true)
// Get cropped image
const fromCanvas = editorRef.current.getImage()
const useCropAndSave = async () => {
let key;
// Get cropped image
if (editorRef.current) {
const fromCanvas = editorRef.current?.getImage()
const toCanvas = document.createElement("canvas")
toCanvas.width = 460
toCanvas.height = 460
@ -35,26 +32,22 @@ const AvatarEditorModal: React.FC<{
const blob = await pica.toBlob(result, "image/jpeg", 0.9)
// Upload image to R2
const { key } = await uploadFile(blob, image!.name)
// Save the image to profile / site
const res = await updateSite({ site, icon: key })
if (res.code === 0) {
setIsOpen(false)
toast.success("Updated!")
} else {
toast.error("Failed to update site" + ": " + res.message)
}
} catch (error: any) {
console.error(error)
toast.error(error.message)
} finally {
setIsSaving(false)
key = (await uploadFile(blob, image!.name)).key
}
// Save the image to profile / site
updateSite.mutate({ site, icon: key })
}
useEffect(() => {
if (updateSite.isSuccess) {
setIsOpen(false)
toast.success("Updated!")
} else if (updateSite.isError) {
toast.error("Failed to update site")
}
}, [updateSite, setIsOpen])
return (
<Dialog
open={isOpen}
@ -81,7 +74,7 @@ const AvatarEditorModal: React.FC<{
</div>
<div className="h-16 border-t flex items-center px-5">
<Button isBlock onClick={cropAndSave} isLoading={isSaving}>
<Button isBlock onClick={useCropAndSave} isLoading={updateSite.isLoading}>
Crop and Save
</Button>
</div>

View File

@ -9,7 +9,7 @@ import { DashboardIcon } from "../icons/DashboardIcon"
import { UniLink } from "../ui/UniLink"
import { DashboardSidebar } from "./DashboardSidebar"
import { SiteSwitcher } from "./SiteSwitcher"
import { getUserSites } from "~/models/site.model"
import { useGetUserSites } from "~/queries/site"
import { useAccount } from 'wagmi'
import type { Profile } from "unidata.js"
@ -25,14 +25,10 @@ export function DashboardLayout({
const { data: viewer } = useAccount()
let [subscriptions, setSubscriptions] = useState<Profile[]>([])
const userSite = useGetUserSites(viewer?.address);
useEffect(() => {
if (viewer?.address) {
getUserSites(viewer.address).then((sites) => {
setSubscriptions(sites || [])
})
} else {
if (!viewer?.address) {
router.push("/")
}
}, [viewer?.address, router])
@ -78,7 +74,7 @@ export function DashboardLayout({
<div className="mb-2">
<SiteSwitcher
subdomain={subdomain}
subscriptions={subscriptions}
subscriptions={userSite.data || []}
viewer={viewer}
/>
</div>

View File

@ -9,7 +9,7 @@ import { useRouter } from "next/router"
import { useStore } from "~/lib/store"
export type SiteLayoutProps = {
site: Profile
site?: Profile
viewer?: Profile | null
subscription?: { email?: boolean } | null
children: React.ReactNode

View File

@ -5,7 +5,7 @@ import { PostMeta } from "./PostMeta"
import { Profile, Note } from "~/lib/types"
export const SitePage: React.FC<{
site: Profile,
site?: Profile,
page: Note
}> = ({ site, page }) => {
return (
@ -20,9 +20,9 @@ export const SitePage: React.FC<{
<PostMeta
publishedAt={page.date_published}
authors={[{
id: site.username || "",
name: site.name || "",
avatar: site.avatars?.[0] || null,
id: site?.username || "",
name: site?.name || "",
avatar: site?.avatars?.[0] || null,
}] || []}
/>
)}

View File

@ -11,7 +11,6 @@ import { Rendered, renderPageContent } from "~/markdown"
import { notFound } from "~/lib/server-side-props"
import { PageVisibilityEnum, Notes } from "~/lib/types"
import { isUUID } from "~/lib/uuid"
import { getSite } from "./site.model"
import { stripHTML } from "~/lib/utils"
import unidata from "~/lib/unidata"

View File

@ -69,8 +69,6 @@ export const getSite = async (input: string) => {
}
site.name = site.name || site.username
if (!site) return null
return site
}

View File

@ -12,6 +12,8 @@ import {
WagmiConfig,
} from 'wagmi'
import { jsonRpcProvider } from 'wagmi/providers/jsonRpc'
import { Hydrate, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import '@rainbow-me/rainbowkit/styles.css'
@ -50,13 +52,20 @@ const wagmiClient = createClient({
provider
})
const queryClient = new QueryClient();
function MyApp({ Component, pageProps }: any) {
return (
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<StoreProvider createStore={createStore}>
<Component {...pageProps} />
<Toaster />
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<ReactQueryDevtools />
<Component {...pageProps} />
<Toaster />
</Hydrate>
</QueryClientProvider>
</StoreProvider>
</RainbowKitProvider>
</WagmiConfig>

View File

@ -7,7 +7,9 @@ import { SitePage } from "~/components/site/SitePage"
import { serverSidePropsHandler } from "~/lib/server-side-props"
import { getViewer } from "~/lib/viewer"
import { Viewer, Profile, Note } from "~/lib/types"
import { getUserSites, getSite } from "~/models/site.model"
import { queryClient, prefetchGetSite } from "~/queries/site.server"
import { useGetSite } from "~/queries/site"
import { dehydrate, QueryClient } from '@tanstack/react-query'
import { getPage } from "~/models/page.model"
import { PageVisibilityEnum } from "~/lib/types"
import { notFound } from "~/lib/server-side-props"
@ -16,9 +18,10 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
async (ctx) => {
const domainOrSubdomain = ctx.params!.site as string
const pageSlug = ctx.params!.page as string
await prefetchGetSite(domainOrSubdomain)
const [site, page] = await Promise.all([
getSite(domainOrSubdomain),
const [page] = await Promise.all([
getPage({
site: domainOrSubdomain,
page: pageSlug,
@ -33,7 +36,7 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
return {
props: {
site,
dehydratedState: dehydrate(queryClient),
page,
domainOrSubdomain,
},
@ -42,23 +45,22 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
)
function SitePagePage({
site,
page,
domainOrSubdomain,
}: {
site: Profile,
page: Note,
domainOrSubdomain: string
}) {
const ogDescription = page.summary?.content || page.body?.content
const site = useGetSite(domainOrSubdomain)
return (
<SiteLayout
site={site!}
site={site.data}
title={page!.title}
ogDescription={ogDescription}
>
<SitePage site={site!} page={page!} />
<SitePage site={site.data} page={page!} />
</SiteLayout>
)
}

View File

@ -7,16 +7,19 @@ import { serverSidePropsHandler } from "~/lib/server-side-props"
import { SiteArchives } from "~/components/site/SiteArchives"
import { getViewer } from "~/lib/viewer"
import { Viewer, Profile, Notes } from "~/lib/types"
import { getSite } from "~/models/site.model"
import { queryClient, prefetchGetSite } from "~/queries/site.server"
import { useGetSite } from "~/queries/site"
import { dehydrate, QueryClient } from '@tanstack/react-query'
import { getPagesBySite } from "~/models/page.model"
import { PageVisibilityEnum } from "~/lib/types"
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
async (ctx) => {
const domainOrSubdomain = ctx.params!.site as string
await prefetchGetSite(domainOrSubdomain)
const [site, posts] = await Promise.all([
getSite(domainOrSubdomain),
const [posts] = await Promise.all([
getPagesBySite({
site: domainOrSubdomain,
take: 1000,
@ -27,7 +30,7 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
return {
props: {
site,
dehydratedState: dehydrate(queryClient),
posts,
domainOrSubdomain,
},
@ -36,16 +39,18 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
)
function SiteArchivesPage({
site,
posts,
domainOrSubdomain,
}: {
site: Profile,
posts: Notes,
domainOrSubdomain: string
}) {
const site = useGetSite(domainOrSubdomain)
return (
<SiteLayout
site={site!}
site={site.data}
title="Archives"
>
<SiteArchives posts={posts} />

View File

@ -7,15 +7,18 @@ import { Viewer, Profile, Notes } from "~/lib/types"
import { getViewer } from "~/lib/viewer"
import { useAccount } from 'wagmi'
import { useEffect, useState } from 'react'
import { getUserSites, getSite } from "~/models/site.model"
import { queryClient, prefetchGetSite } from "~/queries/site.server"
import { useGetSite } from "~/queries/site"
import { dehydrate, QueryClient } from '@tanstack/react-query'
import { getPagesBySite } from "~/models/page.model"
import { PageVisibilityEnum } from "~/lib/types"
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const domainOrSubdomain = ctx.params!.site as string
const [site, posts] = await Promise.all([
getSite(domainOrSubdomain),
await prefetchGetSite(domainOrSubdomain)
const [posts] = await Promise.all([
getPagesBySite({
site: domainOrSubdomain,
take: 1000,
@ -26,7 +29,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
site,
dehydratedState: dehydrate(queryClient),
posts,
domainOrSubdomain,
},
@ -34,15 +37,17 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
}
function SiteIndexPage({
site,
posts,
domainOrSubdomain,
}: {
site: Profile,
posts: Notes,
domainOrSubdomain: string
}) {
const site = useGetSite(domainOrSubdomain)
return (
<SiteLayout site={site!}>
<SiteLayout site={site.data}>
<SiteHome posts={posts} />
</SiteLayout>
)

View File

@ -7,22 +7,14 @@ import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { OUR_DOMAIN } from "~/lib/env"
import { getSite, updateSite } from "~/models/site.model"
import { Profile } from "unidata.js"
import { useGetSite, useUpdateSite } from "~/queries/site"
export default function SettingsDomainsPage() {
const router = useRouter()
const subdomain = router.query.subdomain as string
let [site, setSite] = useState<Profile | null>(null)
useEffect(() => {
if (subdomain) {
getSite(subdomain).then((site) => {
setSite(site)
})
}
}, [subdomain])
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
const form = useForm({
defaultValues: {
@ -30,28 +22,35 @@ export default function SettingsDomainsPage() {
},
})
let [loading, setLoading] = useState<boolean>(false)
const handleSubmit = form.handleSubmit((values) => {
setLoading(true)
updateSite({
updateSite.mutate({
site: subdomain,
subdomain: values.subdomain,
})
updateSite.mutate({
site: subdomain,
subdomain: values.subdomain,
}).then((result) => {
if (result.code === 0) {
toast.success("Saved!")
router.replace(
`/dashboard/${values.subdomain}/settings/domains`
)
} else {
toast.error("Failed to update site" + ": " + result.message)
}
setLoading(false)
})
})
useEffect(() => {
if (site) {
form.setValue("subdomain", site.username || "")
if (updateSite.isSuccess) {
if (updateSite.data?.code === 0) {
toast.success("Saved!")
router.replace(
`/dashboard/${updateSite.variables?.subdomain}/settings/domains`
)
} else {
toast.error("Failed to update site" + ": " + updateSite.data.message)
}
} else if (updateSite.isError) {
toast.error("Failed to update site")
}
}, [updateSite, router])
useEffect(() => {
if (site.isSuccess && site.data) {
form.setValue("subdomain", site.data.username || "")
}
}, [form, site])
@ -70,7 +69,7 @@ export default function SettingsDomainsPage() {
/>
</div>
<div className="mt-5">
<Button type="submit" isLoading={loading}>
<Button type="submit" isLoading={updateSite.isLoading}>
Save
</Button>
</div>

View File

@ -7,23 +7,15 @@ import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { useRouter } from "next/router"
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
import { useForm } from "react-hook-form"
import { getSite, updateSite } from "~/models/site.model"
import { Profile } from "unidata.js"
import { useGetSite, useUpdateSite } from "~/queries/site"
export default function SiteSettingsGeneralPage() {
const router = useRouter()
const subdomain = router.query.subdomain as string
let [site, setSite] = useState<Profile | null>(null)
useEffect(() => {
if (subdomain) {
getSite(subdomain).then((site) => {
setSite(site)
})
}
}, [subdomain])
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
const form = useForm({
defaultValues: {
@ -32,29 +24,32 @@ export default function SiteSettingsGeneralPage() {
},
})
let [loading, setLoading] = useState<boolean>(false)
const handleSubmit = form.handleSubmit((values) => {
setLoading(true)
updateSite({
updateSite.mutate({
site: subdomain,
name: values.name,
description: values.description,
}).then((result) => {
if (result.code === 0) {
toast.success("Site updated")
} else {
toast.error("Failed to update site" + ": " + result.message)
}
setLoading(false)
})
})
useEffect(() => {
if (site) {
form.setValue("name", site.name || "")
form.setValue("description", site.bio || "")
if (updateSite.isSuccess) {
if (updateSite.data?.code === 0) {
toast.success("Site updated")
} else {
toast.error("Failed to update site" + ": " + updateSite.data.message)
}
} else if (updateSite.isError) {
toast.error("Failed to update site")
}
}, [site, form])
}, [updateSite, router])
useEffect(() => {
if (site.data) {
form.setValue("name", site.data.name || "")
form.setValue("description", site.data.bio || "")
}
}, [site.data, form])
return (
<DashboardLayout title="Site Settings">
@ -63,9 +58,9 @@ export default function SiteSettingsGeneralPage() {
<div>
<label className="form-label">Icon</label>
<AvatarForm
site={site.username!}
name={site.name || site.username || ""}
filename={site.avatars?.[0] || undefined}
site={site.data?.username!}
name={site.data?.name || site.data?.username || ""}
filename={site.data?.avatars?.[0] || undefined}
/>
</div>
)}
@ -85,7 +80,7 @@ export default function SiteSettingsGeneralPage() {
/>
</div>
<div className="mt-5">
<Button type="submit" isLoading={loading}>
<Button type="submit" isLoading={updateSite.isLoading}>
Save
</Button>
</div>

View File

@ -9,7 +9,7 @@ import { SiteNavigationItem, Profile } from "~/lib/types"
import { nanoid } from "nanoid"
import { ReactSortable } from "react-sortablejs"
import equal from "fast-deep-equal"
import { getSite, updateSite } from "~/models/site.model"
import { useGetSite, useUpdateSite } from "~/queries/site"
type UpdateItem = (id: string, newItem: Partial<SiteNavigationItem>) => void
@ -70,21 +70,14 @@ export default function SiteSettingsNavigationPage() {
const subdomain = router.query.subdomain as string
let [site, setSite] = useState<Profile | null>(null)
useEffect(() => {
if (subdomain) {
getSite(subdomain).then((site) => {
setSite(site)
})
}
}, [subdomain])
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
const [items, setItems] = useState<SiteNavigationItem[]>([])
const itemsModified = useMemo(() => {
if (!site) return false
return !equal(items, site.navigation)
if (!site.isSuccess) return false
return !equal(items, site.data.navigation)
}, [items, site])
const updateItem: UpdateItem = (id, newItem) => {
@ -102,32 +95,35 @@ export default function SiteSettingsNavigationPage() {
setItems((items) => [...items, { id: nanoid(), label: "", url: "" }])
}
let [loading, setLoading] = useState<boolean>(false)
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
updateSite({
site: site!.username!,
updateSite.mutate({
site: site.data?.username!,
navigation: items,
}).then((result) => {
if (result.code === 0) {
toast.success("Saved")
} else {
toast.error("Failed to save" + ": " + result.message)
}
setLoading(false)
})
}
useEffect(() => {
if (updateSite.isSuccess) {
if (updateSite.data?.code === 0) {
toast.success("Saved")
} else {
toast.error("Failed to save" + ": " + updateSite.data.message)
}
} else if (updateSite.isError) {
toast.error("Failed to save")
}
}, [updateSite, router])
const removeItem: RemoveItem = (id) => {
setItems((items) => items.filter((item) => item.id !== id))
}
useEffect(() => {
if (site?.navigation) {
setItems(site.navigation)
if (site.data?.navigation) {
setItems(site.data?.navigation)
}
}, [site?.navigation])
}, [site.data?.navigation])
return (
<DashboardLayout title="Navigation">
@ -164,7 +160,7 @@ export default function SiteSettingsNavigationPage() {
<div className="border-t pt-5 mt-10 space-x-3 flex items-center">
<Button
type="submit"
isLoading={loading}
isLoading={updateSite.isLoading}
isDisabled={!itemsModified}
>
Save

View File

@ -1,5 +1,5 @@
import { redirect } from "~/lib/server-side-props"
import { getUserSites } from "~/models/site.model"
import { useGetUserSites } from "~/queries/site"
import { useAccount } from 'wagmi'
import { useEffect } from "react"
import { useRouter } from 'next/router'
@ -7,20 +7,17 @@ import { useRouter } from 'next/router'
export default function Dashboard() {
const { data: wagmiData } = useAccount()
const router = useRouter()
const userSites = useGetUserSites(wagmiData?.address)
useEffect(() => {
if (wagmiData?.address) {
getUserSites(wagmiData.address).then((sites) => {
if (!sites) {
router.push(`/dashboard/new-site`)
} else {
router.push(`/dashboard/${sites[0].username}`)
}
})
} else {
router.push("/")
if (userSites.isSuccess) {
if (!userSites.data?.length) {
router.push(`/dashboard/new-site`)
} else {
router.push(`/dashboard/${userSites.data[0].username}`)
}
}
}, [wagmiData?.address, router])
}, [userSites, router])
return <div>Loading...</div>
}

View File

@ -8,10 +8,11 @@ import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { APP_NAME, OUR_DOMAIN } from "~/lib/env"
import { useAccount } from "wagmi"
import { createSite } from "~/models/site.model"
import { useCreateSite } from "~/queries/site"
export default function NewSitePage() {
const router = useRouter()
const createSite = useCreateSite()
const [address, setAddress] = useState<string>('')
const { data: wagmiData } = useAccount()
@ -31,18 +32,25 @@ export default function NewSitePage() {
},
})
let [loading, setLoading] = useState<boolean>(false)
const handleSubmit = form.handleSubmit(async (values) => {
setLoading(true)
const result = await createSite(wagmiData!.address!, values)
setLoading(false)
if (result.code === 0) {
router.push(`/dashboard/${values.subdomain}`)
} else {
toast.error("Failed to create site" + ": " + result.message)
}
createSite.mutate({
address: wagmiData!.address!,
payload: values,
})
})
useEffect(() => {
if (createSite.isSuccess) {
if (createSite.data?.code === 0) {
router.push(`/dashboard/${createSite.variables?.payload.subdomain}`)
} else {
toast.error("Failed to create site" + ": " + createSite.data.message)
}
} else if (createSite.isError) {
toast.error("Failed to create site")
}
}, [createSite, router])
return (
<>
<SEOHead title="New Site" siteName={APP_NAME} />
@ -96,7 +104,7 @@ export default function NewSitePage() {
/>
</div>
<div>
<Button type="submit" isBlock isLoading={loading}>
<Button type="submit" isBlock isLoading={createSite.isLoading}>
Create
</Button>
</div>

View File

@ -0,0 +1,13 @@
import * as siteModel from "~/models/site.model"
import { QueryClient } from '@tanstack/react-query'
const queryClientServer = new QueryClient()
const cacheMaxAge = parseInt(process.env.CACHE_MAX_AGE || "600000")
export const queryClient = queryClientServer
export const prefetchGetSite = async (input: string) => {
await queryClientServer.prefetchQuery(['getSite', input], async () => {
return siteModel.getSite(input)
}, { staleTime: cacheMaxAge })
}

53
src/queries/site.ts Normal file
View File

@ -0,0 +1,53 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as siteModel from "~/models/site.model"
import { SiteNavigationItem, Profile } from "~/lib/types"
export const useGetUserSites = (address?: string) => {
return useQuery(['getUserSites', address], async () => {
if (!address) {
return []
}
return siteModel.getUserSites(address)
})
}
export const useGetSite = (input: string) => {
return useQuery(['getSite', input], async () => {
return siteModel.getSite(input)
})
}
export function useUpdateSite() {
const queryClient = useQueryClient();
const mutation = useMutation(async (
payload: {
site: string
name?: string
description?: string
icon?: string | null
subdomain?: string
navigation?: SiteNavigationItem[]
}) => {
return siteModel.updateSite(payload)
}, {
onSuccess: (data, variables) => {
queryClient.invalidateQueries(['getUserSites'])
queryClient.invalidateQueries(['getSite'])
},
})
return mutation
}
export function useCreateSite() {
const queryClient = useQueryClient();
return useMutation(async (input: {
address: string,
payload: { name: string; subdomain: string },
}) => {
return siteModel.createSite(input.address, input.payload)
}, {
onSuccess: (data, variables) => {
queryClient.invalidateQueries(['getUserSites', variables.address])
},
})
}