feat: use react query for page requests

This commit is contained in:
DIYgod 2022-07-22 01:30:44 +01:00
parent 074611e88d
commit c19e1d8f1a
No known key found for this signature in database
GPG Key ID: D159328F47A80DCA
14 changed files with 181 additions and 134 deletions

View File

@ -11,7 +11,7 @@ import Link from "next/link"
import toast from "react-hot-toast"
import { EmptyState } from "../ui/EmptyState"
import type { Note } from "unidata.js"
import { getPagesBySite, deletePage } from "~/models/page.model"
import { useGetPagesBySite, useDeletePage } from "~/queries/page"
export const PagesManager: React.FC<{
isPost: boolean
@ -27,39 +27,21 @@ export const PagesManager: React.FC<{
[router.query.visibility]
)
let [pages, setPages] = useState<Note[] | null>(null)
const pageDelete = (id: string) => {
deletePage({
site: subdomain,
id: id,
}).then(() => {
toast.success("Deleted!")
getPagesBySite({
type: isPost ? "post" : "page",
site: subdomain,
take: 1000,
visibility,
render: true,
}).then((pages) => {
setPages(pages?.list || [])
})
})
}
const deletePage = useDeletePage()
useEffect(() => {
if (subdomain) {
getPagesBySite({
type: isPost ? "post" : "page",
site: subdomain!,
take: 1000,
visibility,
render: true,
}).then((pages) => {
setPages(pages?.list || [])
})
if (deletePage.isSuccess) {
toast.success("Deleted!")
}
}, [subdomain, isPost, visibility])
}, [deletePage.isSuccess])
const pages = useGetPagesBySite({
type: isPost ? "post" : "page",
site: subdomain!,
take: 1000,
visibility,
render: true,
})
const tabItems: TabItem[] = [
{
@ -186,11 +168,11 @@ export const PagesManager: React.FC<{
<Tabs items={tabItems} />
<div className="-mt-3">
{pages && pages.length === 0 && (
{pages.data?.list.length === 0 && (
<EmptyState resource={isPost ? "posts" : "pages"} />
)}
{pages?.map((page) => {
{pages.data?.list?.map((page) => {
return (
<Link key={page.id} href={getPageEditLink(page)}>
<a className="group relative hover:bg-zinc-100 rounded-lg py-3 px-3 transition-colors -mx-3 flex justify-between">

View File

@ -5,7 +5,7 @@ import { EmptyState } from "../ui/EmptyState"
import { Notes, Note } from "~/lib/types"
export const SiteArchives: React.FC<{
posts: Notes
posts?: Notes
}> = ({ posts }) => {
const groupedByYear = useMemo<Map<string, Note[]>>(() => {
const map = new Map()

View File

@ -6,17 +6,17 @@ import { Profile, Note } from "~/lib/types"
export const SitePage: React.FC<{
site?: Profile,
page: Note
page?: Note
}> = ({ site, page }) => {
return (
<>
<div className="">
{page.tags?.includes("post") ? (
{page?.tags?.includes("post") ? (
<h2 className="text-4xl font-bold">{page.title}</h2>
) : (
<h2 className="text-xl font-bold page-title">{page.title}</h2>
<h2 className="text-xl font-bold page-title">{page?.title}</h2>
)}
{page.tags?.includes("post") && (
{page?.tags?.includes("post") && (
<PostMeta
publishedAt={page.date_published}
authors={[{
@ -28,28 +28,28 @@ export const SitePage: React.FC<{
)}
</div>
<PageContent
contentHTML={page.body?.content || ""}
contentHTML={page?.body?.content || ""}
className="my-10"
/>
<div className="bg-gray-50 rounded-lg py-8 px-5 break-all">
<p className="text-gray-700 mb-4">🔔 This {page.tags?.includes("post") ? "post" : "page"} has been permanently stored on-chain and signed by its creator.</p>
<p className="text-gray-700 mb-4">🔔 This {page?.tags?.includes("post") ? "post" : "page"} has been permanently stored on-chain and signed by its creator.</p>
<ul className="text-gray-600 text-sm leading-6">
<li className="mt-2">
<div className="font-bold">Crossbell Transaction</div>
<div>{page.related_urls?.filter((url) => url.startsWith("https://scan.crossbell.io/tx/")).map((url) => {
<div>{page?.related_urls?.filter((url) => url.startsWith("https://scan.crossbell.io/tx/")).map((url) => {
return <a target="_blank" rel="noreferrer" className="inline-block mr-4 break-all" href={url} key={url}>{url.replace("https://scan.crossbell.io/tx/", "").slice(0, 10)}...{url.replace("https://scan.crossbell.io/tx/", "").slice(-10)}</a>
})}</div>
</li>
<li className="mt-2">
<div className="font-bold">IPFS Address</div>
<div>{page.related_urls?.filter((url) => url.startsWith("https://gateway.ipfs.io/ipfs/")).map((url) => {
<div>{page?.related_urls?.filter((url) => url.startsWith("https://gateway.ipfs.io/ipfs/")).map((url) => {
return <a target="_blank" rel="noreferrer" className="inline-block mr-4 break-all" href={url} key={url}>{url.replace("https://gateway.ipfs.io/ipfs/", "ipfs://")}</a>
})}</div>
</li>
<li className="mt-2">
<div className="font-bold">Author Address</div>
<div>
<a target="_blank" rel="noreferrer" className="inline-block mr-4 break-all" href={`https://scan.crossbell.io/address/${page.metadata?.owner}`} key={page.metadata?.owner}>{page.metadata?.owner}</a>
<a target="_blank" rel="noreferrer" className="inline-block mr-4 break-all" href={`https://scan.crossbell.io/address/${page?.metadata?.owner}`} key={page?.metadata?.owner}>{page?.metadata?.owner}</a>
</div>
</li>
</ul>

View File

@ -119,7 +119,7 @@ export async function scheduleEmailForPost(
export async function getPagesBySite(
input: {
site: string
site?: string
type: "post" | "page"
visibility?: PageVisibilityEnum | null
take?: number | null
@ -138,12 +138,15 @@ export async function getPagesBySite(
const visibility = input.visibility || PageVisibilityEnum.All
let pages: Notes | null = await unidata.notes.get({
let pages: Notes = await unidata.notes.get({
source: 'Crossbell Note',
identity: input.site,
platform: 'Crossbell',
limit: input.take || 1000,
});
}) || {
total: 0,
list: [],
};
if (pages?.list) {
switch (visibility) {
@ -203,8 +206,8 @@ export async function getPage<TRender extends boolean = false>(
includeAuthors?: boolean
},
) {
if (!input.site) {
throw notFound(`site not found`)
if (!input.site || !(input.page || input.pageId)) {
return undefined
}
const pages: Notes | null = await unidata.notes.get({

View File

@ -10,7 +10,8 @@ import { Viewer, Profile, Note } from "~/lib/types"
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 { fetchGetPage } from "~/queries/page.server"
import { useGetPage } from "~/queries/page"
import { PageVisibilityEnum } from "~/lib/types"
import { notFound } from "~/lib/server-side-props"
@ -20,15 +21,12 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
const pageSlug = ctx.params!.page as string
await prefetchGetSite(domainOrSubdomain)
const [page] = await Promise.all([
getPage({
site: domainOrSubdomain,
page: pageSlug,
render: true,
includeAuthors: true,
}),
])
const page = await fetchGetPage({
site: domainOrSubdomain,
page: pageSlug,
render: true,
includeAuthors: true,
})
if (new Date(page.date_published) > new Date()) {
throw notFound()
@ -37,30 +35,37 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
return {
props: {
dehydratedState: dehydrate(queryClient),
page,
domainOrSubdomain,
pageSlug,
},
}
},
)
function SitePagePage({
page,
domainOrSubdomain,
pageSlug,
}: {
page: Note,
domainOrSubdomain: string
pageSlug: string
}) {
const ogDescription = page.summary?.content || page.body?.content
const page = useGetPage({
site: domainOrSubdomain,
page: pageSlug,
render: true,
includeAuthors: true,
})
const ogDescription = page.data?.summary?.content || page.data?.body?.content
const site = useGetSite(domainOrSubdomain)
return (
<SiteLayout
site={site.data}
title={page!.title}
title={page.data?.title}
ogDescription={ogDescription}
>
<SitePage site={site.data} page={page!} />
<SitePage site={site.data} page={page.data} />
</SiteLayout>
)
}

View File

@ -10,7 +10,8 @@ import { Viewer, Profile, Notes } from "~/lib/types"
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 { useGetPagesBySite } from "~/queries/page"
import { prefetchGetPagesBySite } from "~/queries/page.server"
import { PageVisibilityEnum } from "~/lib/types"
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
@ -18,20 +19,16 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
const domainOrSubdomain = ctx.params!.site as string
await prefetchGetSite(domainOrSubdomain)
const [posts] = await Promise.all([
getPagesBySite({
site: domainOrSubdomain,
take: 1000,
type: "post",
visibility: PageVisibilityEnum.Published,
}),
])
await prefetchGetPagesBySite({
site: domainOrSubdomain,
take: 1000,
type: "post",
visibility: PageVisibilityEnum.Published,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
posts,
domainOrSubdomain,
},
}
@ -39,7 +36,6 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
)
function SiteArchivesPage({
posts,
domainOrSubdomain,
}: {
site: Profile,
@ -47,13 +43,19 @@ function SiteArchivesPage({
domainOrSubdomain: string
}) {
const site = useGetSite(domainOrSubdomain)
const posts = useGetPagesBySite({
site: domainOrSubdomain,
take: 1000,
type: "post",
visibility: PageVisibilityEnum.Published,
})
return (
<SiteLayout
site={site.data}
title="Archives"
>
<SiteArchives posts={posts} />
<SiteArchives posts={posts.data} />
</SiteLayout>
)
}

View File

@ -10,45 +10,45 @@ import { useEffect, useState } from 'react'
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 { useGetPagesBySite } from "~/queries/page"
import { prefetchGetPagesBySite } from "~/queries/page.server"
import { PageVisibilityEnum } from "~/lib/types"
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const domainOrSubdomain = ctx.params!.site as string
await prefetchGetSite(domainOrSubdomain)
const [posts] = await Promise.all([
getPagesBySite({
site: domainOrSubdomain,
take: 1000,
type: "post",
visibility: PageVisibilityEnum.Published,
}),
])
await prefetchGetPagesBySite({
site: domainOrSubdomain,
take: 1000,
type: "post",
visibility: PageVisibilityEnum.Published,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
posts,
domainOrSubdomain,
},
}
}
function SiteIndexPage({
posts,
domainOrSubdomain,
}: {
site: Profile,
posts: Notes,
domainOrSubdomain: string
}) {
const site = useGetSite(domainOrSubdomain)
const posts = useGetPagesBySite({
site: domainOrSubdomain,
take: 1000,
type: "post",
visibility: PageVisibilityEnum.Published,
})
return (
<SiteLayout site={site.data}>
<SiteHome posts={posts} />
<SiteHome posts={posts.data} />
</SiteLayout>
)
}

View File

@ -21,7 +21,7 @@ import { PageVisibilityEnum, Note } from "~/lib/types"
import { FieldLabel } from "~/components/ui/FieldLabel"
import { Button } from "~/components/ui/Button"
import { useStore } from "~/lib/store"
import { getPage, createOrUpdatePage } from "~/models/page.model"
import { useGetPage, useCreateOrUpdatePage } from "~/queries/page"
const getInputDatetimeValue = (date: Date | string) => {
const str = dayjs(date).format()
@ -35,29 +35,19 @@ export default function SubdomainEditor() {
const isPost = router.query.type === "post"
const pageId = router.query.id as string | undefined
let [page, setPage] = useState<Note | undefined>(undefined)
const page = useGetPage({
site: subdomain!,
pageId: pageId,
render: false
})
useEffect(() => {
if (subdomain && pageId) {
getPage({
site: subdomain!,
pageId: pageId,
render: false
}).then((page) => {
setPage(page)
})
}
}, [subdomain, isPost, pageId])
const pageContent = useMemo(() => page?.body?.content, [page?.body?.content])
const pageContent = useMemo(() => page.data?.body?.content, [page.data?.body?.content])
const visibility = getPageVisibility({
date_published: page?.date_published,
date_published: page.data?.date_published,
})
const published = visibility !== PageVisibilityEnum.Draft
const [createOrUpdatePageStatus, setCreateOrUpdatePageStatus] = useState<string>('')
const uploadFile = useUploadFile()
const [values, setValues] = useState({
@ -83,26 +73,29 @@ export default function SubdomainEditor() {
[setValues, values],
)
const createOrUpdatePage = useCreateOrUpdatePage()
const savePage = (published: boolean) => {
setCreateOrUpdatePageStatus("loading")
createOrUpdatePage({
createOrUpdatePage.mutate({
...values,
siteId: subdomain,
pageId: pageId,
isPost: isPost,
published,
content,
}).then((result) => {
if (result && result.code === 0) {
setCreateOrUpdatePageStatus("success")
toast.success(values.published ? "Updated" : "Saved!")
} else {
setCreateOrUpdatePageStatus("error")
toast.error(result.message)
}
})
}
useEffect(() => {
if (createOrUpdatePage.data) {
if (createOrUpdatePage.data?.code === 0) {
toast.success(values.published ? "Updated" : "Saved!")
} else {
toast.error("Error: " + createOrUpdatePage.data?.message)
}
}
}, [createOrUpdatePage.data, values.published])
const handleDropFile = useCallback(
async (file: File, view: EditorView) => {
const toastId = toast.loading("Uploading...")
@ -141,16 +134,16 @@ export default function SubdomainEditor() {
})
useEffect(() => {
if (!page) return
if (!page.data) return
setValues({
title: page.title || '',
publishedAt: page.date_published === new Date('9999-01-01').toISOString() ? new Date().toISOString() : page.date_published,
published: page.date_published !== new Date('9999-01-01').toISOString(),
excerpt: page.summary?.content || "",
slug: page.slug!,
title: page.data.title || '',
publishedAt: page.data.date_published === new Date('9999-01-01').toISOString() ? new Date().toISOString() : page.data.date_published,
published: page.data.date_published !== new Date('9999-01-01').toISOString(),
excerpt: page.data.summary?.content || "",
slug: page.data.slug!,
})
}, [page])
}, [page.data])
useEffect(() => {
if (typeof pageContent === "string") {
@ -186,7 +179,7 @@ export default function SubdomainEditor() {
<PublishButton
save={savePage}
published={published}
isSaving={createOrUpdatePageStatus === "loading"}
isSaving={createOrUpdatePage.isLoading}
/>
</div>
</header>

View File

@ -52,7 +52,7 @@ export default function SettingsDomainsPage() {
if (site.isSuccess && site.data) {
form.setValue("subdomain", site.data.username || "")
}
}, [form, site])
}, [form, site.data, site.isSuccess])
const title = "Site Settings"
return (

View File

@ -78,7 +78,7 @@ export default function SiteSettingsNavigationPage() {
const itemsModified = useMemo(() => {
if (!site.isSuccess) return false
return !equal(items, site.data.navigation)
}, [items, site])
}, [items, site.data, site.isSuccess])
const updateItem: UpdateItem = (id, newItem) => {
setItems((items) => {

View File

@ -0,0 +1,17 @@
import * as pageModel from "~/models/page.model"
import { queryClientServer, cacheMaxAge } from "~/queries/server"
export const queryClient = queryClientServer
export const fetchGetPage = async (input: Parameters<typeof pageModel.getPage>[0]) => {
return await queryClientServer.fetchQuery(['getPage', input.page, input], async () => {
return pageModel.getPage(input)
}, { staleTime: cacheMaxAge })
}
export const prefetchGetPagesBySite = async (input: Parameters<typeof pageModel.getPagesBySite>[0]) => {
await queryClientServer.fetchQuery(['getPagesBySite', input.site, input], async () => {
return pageModel.getPagesBySite(input)
}, { staleTime: cacheMaxAge })
}

41
src/queries/page.ts Normal file
View File

@ -0,0 +1,41 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as pageModel from "~/models/page.model"
import { PageVisibilityEnum } from "~/lib/types"
export const useGetPagesBySite = (input: Parameters<typeof pageModel.getPagesBySite>[0]) => {
return useQuery(['getPagesBySite', input.site, input], async () => {
return pageModel.getPagesBySite(input)
})
}
export const useGetPage = (input: Parameters<typeof pageModel.getPage>[0]) => {
return useQuery(['getPage', input.page, input], async () => {
return pageModel.getPage(input)
})
}
export function useCreateOrUpdatePage() {
const queryClient = useQueryClient();
const mutation = useMutation(async (
payload: Parameters<typeof pageModel.createOrUpdatePage>[0]) => {
return pageModel.createOrUpdatePage(payload)
}, {
onSuccess: (data, variables) => {
queryClient.invalidateQueries(['getPagesBySite', variables.siteId])
queryClient.invalidateQueries(['getPage', variables.pageId])
},
})
return mutation
}
export function useDeletePage() {
const queryClient = useQueryClient();
return useMutation(async (input: Parameters<typeof pageModel.deletePage>[0]) => {
return pageModel.deletePage(input)
}, {
onSuccess: (data, variables) => {
queryClient.invalidateQueries(['getPagesBySite', variables.site])
queryClient.invalidateQueries(['getPage', variables.id])
},
})
}

7
src/queries/server.ts Normal file
View File

@ -0,0 +1,7 @@
import { QueryClient } from '@tanstack/react-query'
const queryClientServerIn = new QueryClient()
export const cacheMaxAge = parseInt(process.env.CACHE_MAX_AGE || "600000")
export const queryClientServer = queryClientServerIn

View File

@ -1,8 +1,5 @@
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")
import { queryClientServer, cacheMaxAge } from "~/queries/server"
export const queryClient = queryClientServer