From 074611e88d7f926f4f54ae1f3906b72ccb7fee38 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 21 Jul 2022 22:45:32 +0100 Subject: [PATCH] feat: use react query for site requests --- .env.example | 2 + package.json | 5 +- pnpm-lock.yaml | 91 ++++++++++++------- src/components/dashboard/AvatarForm.tsx | 51 +++++------ src/components/dashboard/DashboardLayout.tsx | 12 +-- src/components/site/SiteLayout.tsx | 2 +- src/components/site/SitePage.tsx | 8 +- src/models/page.model.ts | 1 - src/models/site.model.ts | 2 - src/pages/_app.tsx | 13 ++- src/pages/_site/[site]/[page].tsx | 18 ++-- src/pages/_site/[site]/archives.tsx | 17 ++-- src/pages/_site/[site]/index.tsx | 17 ++-- .../[subdomain]/settings/domains.tsx | 53 ++++++----- .../[subdomain]/settings/general.tsx | 53 +++++------ .../[subdomain]/settings/navigation.tsx | 50 +++++----- src/pages/dashboard/index.tsx | 21 ++--- src/pages/dashboard/new-site.tsx | 30 +++--- src/queries/site.server.ts | 13 +++ src/queries/site.ts | 53 +++++++++++ 20 files changed, 302 insertions(+), 210 deletions(-) create mode 100644 src/queries/site.server.ts create mode 100644 src/queries/site.ts diff --git a/.env.example b/.env.example index 7065438d..2d2bdc5e 100644 --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/package.json b/package.json index ff883710..02c304b1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6873eba..74f1e35e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/dashboard/AvatarForm.tsx b/src/components/dashboard/AvatarForm.tsx index 76e50ace..16624996 100644 --- a/src/components/dashboard/AvatarForm.tsx +++ b/src/components/dashboard/AvatarForm.tsx @@ -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(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 (
-
diff --git a/src/components/dashboard/DashboardLayout.tsx b/src/components/dashboard/DashboardLayout.tsx index 9b46d7f8..4a2ace59 100644 --- a/src/components/dashboard/DashboardLayout.tsx +++ b/src/components/dashboard/DashboardLayout.tsx @@ -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([]) + 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({
diff --git a/src/components/site/SiteLayout.tsx b/src/components/site/SiteLayout.tsx index 97e82ca3..25aa36b5 100644 --- a/src/components/site/SiteLayout.tsx +++ b/src/components/site/SiteLayout.tsx @@ -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 diff --git a/src/components/site/SitePage.tsx b/src/components/site/SitePage.tsx index 3e35ee65..c0571197 100644 --- a/src/components/site/SitePage.tsx +++ b/src/components/site/SitePage.tsx @@ -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<{ )} diff --git a/src/models/page.model.ts b/src/models/page.model.ts index ec15738f..97272a94 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -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" diff --git a/src/models/site.model.ts b/src/models/site.model.ts index e2c5f801..3952c307 100644 --- a/src/models/site.model.ts +++ b/src/models/site.model.ts @@ -69,8 +69,6 @@ export const getSite = async (input: string) => { } site.name = site.name || site.username - if (!site) return null - return site } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 97f1a94c..3e8a6dc6 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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 ( - - + + + + + + + diff --git a/src/pages/_site/[site]/[page].tsx b/src/pages/_site/[site]/[page].tsx index a647e21a..eeb0343b 100644 --- a/src/pages/_site/[site]/[page].tsx +++ b/src/pages/_site/[site]/[page].tsx @@ -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 ( - + ) } diff --git a/src/pages/_site/[site]/archives.tsx b/src/pages/_site/[site]/archives.tsx index bc5f285b..ea1f6bd4 100644 --- a/src/pages/_site/[site]/archives.tsx +++ b/src/pages/_site/[site]/archives.tsx @@ -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 ( diff --git a/src/pages/_site/[site]/index.tsx b/src/pages/_site/[site]/index.tsx index 7692ceae..cc6c1199 100644 --- a/src/pages/_site/[site]/index.tsx +++ b/src/pages/_site/[site]/index.tsx @@ -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 ( - + ) diff --git a/src/pages/dashboard/[subdomain]/settings/domains.tsx b/src/pages/dashboard/[subdomain]/settings/domains.tsx index 0ffa26b4..1979f96d 100644 --- a/src/pages/dashboard/[subdomain]/settings/domains.tsx +++ b/src/pages/dashboard/[subdomain]/settings/domains.tsx @@ -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(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(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() { />
-
diff --git a/src/pages/dashboard/[subdomain]/settings/general.tsx b/src/pages/dashboard/[subdomain]/settings/general.tsx index e8b0b917..ea9974ab 100644 --- a/src/pages/dashboard/[subdomain]/settings/general.tsx +++ b/src/pages/dashboard/[subdomain]/settings/general.tsx @@ -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(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(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 ( @@ -63,9 +58,9 @@ export default function SiteSettingsGeneralPage() {
)} @@ -85,7 +80,7 @@ export default function SiteSettingsGeneralPage() { />
-
diff --git a/src/pages/dashboard/[subdomain]/settings/navigation.tsx b/src/pages/dashboard/[subdomain]/settings/navigation.tsx index bb7c6389..6e3a7e9a 100644 --- a/src/pages/dashboard/[subdomain]/settings/navigation.tsx +++ b/src/pages/dashboard/[subdomain]/settings/navigation.tsx @@ -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) => void @@ -70,21 +70,14 @@ export default function SiteSettingsNavigationPage() { const subdomain = router.query.subdomain as string - let [site, setSite] = useState(null) - - useEffect(() => { - if (subdomain) { - getSite(subdomain).then((site) => { - setSite(site) - }) - } - }, [subdomain]) + const updateSite = useUpdateSite() + const site = useGetSite(subdomain) const [items, setItems] = useState([]) 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(false) const handleSubmit = (e: FormEvent) => { 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 ( @@ -164,7 +160,7 @@ export default function SiteSettingsNavigationPage() {
-
diff --git a/src/queries/site.server.ts b/src/queries/site.server.ts new file mode 100644 index 00000000..eb27974f --- /dev/null +++ b/src/queries/site.server.ts @@ -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 }) +} \ No newline at end of file diff --git a/src/queries/site.ts b/src/queries/site.ts new file mode 100644 index 00000000..7789d8e0 --- /dev/null +++ b/src/queries/site.ts @@ -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]) + }, + }) +}