From 98893e661dca54da78260e87f060bb2ee0665578 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Wed, 14 Jun 2023 22:58:47 +0100 Subject: [PATCH] feat: use crossbell kit to create and update page --- src/app/dashboard/[subdomain]/editor/page.tsx | 115 +++++---- src/components/dashboard/OptionsButton.tsx | 8 +- src/components/dashboard/PagesManager.tsx | 1 - .../PagesManagerBatchSelectActionTab.tsx | 65 ++--- src/components/dashboard/PagesManagerMenu.tsx | 82 +++---- src/models/page.model.ts | 109 --------- src/queries/page.ts | 224 ++++++++++++++++-- 7 files changed, 329 insertions(+), 275 deletions(-) diff --git a/src/app/dashboard/[subdomain]/editor/page.tsx b/src/app/dashboard/[subdomain]/editor/page.tsx index b11498bf..3c55f0a3 100644 --- a/src/app/dashboard/[subdomain]/editor/page.tsx +++ b/src/app/dashboard/[subdomain]/editor/page.tsx @@ -57,9 +57,11 @@ import { cn, pick } from "~/lib/utils" import { Rendered, renderPageContent } from "~/markdown" import { checkPageSlug } from "~/models/page.model" import { - useCreateOrUpdatePage, + useCreatePage, + useDeletePage, useGetDistinctNoteTagsOfCharacter, useGetPage, + useUpdatePage, } from "~/queries/page" import { useGetSite } from "~/queries/site" @@ -186,12 +188,13 @@ export default function SubdomainEditor() { [isPost, queryClient, subdomain, visibility], ) - const createOrUpdatePage = useCreateOrUpdatePage() + const createPage = useCreatePage() + const updatePage = useUpdatePage() const isMobileLayout = useIsMobileLayout() const [isRendering, setIsRendering] = useState(!isMobileLayout) - const savePage = async (published: boolean) => { + const savePage = async () => { const check = await checkPageSlug({ slug: values.slug || defaultSlug, characterId: site.data?.characterId, @@ -201,58 +204,87 @@ export default function SubdomainEditor() { toast.error(check) } else { const uniqueTags = Array.from(new Set(values.tags.split(","))).join(",") - createOrUpdatePage.mutate({ + + const baseValues = { ...values, tags: uniqueTags, slug: values.slug || defaultSlug, - siteId: subdomain, - ...(visibility === PageVisibilityEnum.Draft - ? {} - : { pageId: `${page?.data?.characterId}-${page?.data?.noteId}` }), - isPost: isPost, - published, - applications: page.data?.metadata?.content?.sources, characterId: site.data?.characterId, cover: values.cover, disableAISummary: values.disableAISummary, - }) + } + if (visibility === PageVisibilityEnum.Draft) { + createPage.mutate({ + ...baseValues, + isPost: isPost, + }) + } else { + updatePage.mutate({ + ...baseValues, + noteId: page?.data?.noteId, + }) + } + } + } + + const deleteP = useDeletePage() + const deletePage = async () => { + if (page.data) { + if (!page.data?.noteId) { + // Is draft + delStorage(`draft-${page.data.characterId}-${page.data.draftKey}`) + } else { + // Is Note + return deleteP.mutateAsync({ + site: subdomain, + id: `${page.data.characterId}-${page.data.noteId}`, + characterId: page.data.characterId, + }) + } } } const [isCheersOpen, setIsCheersOpen] = useState(false) useEffect(() => { - if (createOrUpdatePage.isSuccess) { - if (createOrUpdatePage.data?.code === 0) { - if (draftKey) { - delStorage(draftKey) - queryClient.invalidateQueries([ - "getPagesBySite", - site.data?.characterId, - ]) - queryClient.invalidateQueries([ - "getPage", - draftKey.replace(`draft-${site.data?.characterId}-`, ""), - ]) - } else { - queryClient.invalidateQueries(["getPage", pageId]) - } - - if (createOrUpdatePage.data.data) { - router.replace( - `/dashboard/${subdomain}/editor?id=${site.data?.characterId}-${ - createOrUpdatePage.data.data - }&type=${searchParams?.get("type")}`, - ) - } - - setIsCheersOpen(true) - showConfetti() + if (createPage.isSuccess || updatePage.isSuccess) { + if (draftKey) { + delStorage(draftKey) + queryClient.invalidateQueries([ + "getPagesBySite", + site.data?.characterId, + ]) + queryClient.invalidateQueries([ + "getPage", + draftKey.replace(`draft-${site.data?.characterId}-`, ""), + ]) } else { - toast.error("Error: " + createOrUpdatePage.data?.message) + queryClient.invalidateQueries(["getPage", pageId]) } + + if (createPage.data?.noteId) { + router.replace( + `/dashboard/${subdomain}/editor?id=${ + createPage.data?.noteId + }&type=${searchParams?.get("type")}`, + ) + } + + setIsCheersOpen(true) + showConfetti() + + createPage.reset() + updatePage.reset() } - }, [createOrUpdatePage.isSuccess]) + }, [createPage.isSuccess, updatePage.isSuccess]) + + useEffect(() => { + if (createPage.isError || updatePage.isError) { + toast.error("Error: " + (createPage.error || updatePage.error)) + createPage.reset() + updatePage.reset() + } + }, [createPage.isError, updatePage.isSuccess]) useEffect(() => { if (!page.data || !draftKey) return @@ -500,6 +532,7 @@ export default function SubdomainEditor() { void renderPage: Dispatch> - savePage: (published: boolean) => any + savePage: () => any + deletePage: () => any propertiesWidget: React.ReactNode isRendering: boolean published: boolean @@ -109,7 +111,7 @@ export const OptionsButton = ({ className={`${ active ? "bg-accent text-white" : "text-gray-900" } group flex w-full items-center rounded-md px-2 py-2 text-sm`} - onClick={() => savePage(true)} + onClick={() => savePage()} > {t(published ? "Update" : "Publish")} @@ -152,7 +154,7 @@ export const OptionsButton = ({ open={deleteConfirmModalOpen} setOpen={setDeleteConfirmModalOpen} onConfirm={() => { - savePage(false) + deletePage() }} isPost={isPost} /> diff --git a/src/components/dashboard/PagesManager.tsx b/src/components/dashboard/PagesManager.tsx index 36c8fe83..78acb1a0 100644 --- a/src/components/dashboard/PagesManager.tsx +++ b/src/components/dashboard/PagesManager.tsx @@ -222,7 +222,6 @@ export const PagesManager = ({ isPost }: { isPost: boolean }) => { {batchSelected.length > 0 ? ( @@ -41,7 +38,7 @@ export const PagesManagerBatchSelectActionTab = ({ const queryClient = useQueryClient() // Convert or Delete page - const createOrUpdatePage = useCreateOrUpdatePage() + const updatePage = useUpdatePage() const deletePage = useDeletePage() const tabItems: TabItem[] = [ @@ -65,13 +62,7 @@ export const PagesManagerBatchSelectActionTab = ({ }, }, { - text: - "Convert to " + - (isNotxLogContent - ? `${APP_NAME} ${isPost ? "Post" : "Page"}` - : isPost - ? "Page" - : "Post"), + text: "Convert to " + (isPost ? "Page" : "Post"), onClick: async () => { // Start message const toastId = toast.loading("Converting...") @@ -90,48 +81,22 @@ export const PagesManagerBatchSelectActionTab = ({ try { await Promise.all( selectedPages.map((page) => { - // Check again to ensure it's not - const isNotxLogContent = - !page.metadata?.content?.sources?.includes("xlog") - - const targetNoteBase = { - published: true, - pageId: `${page.characterId}-${page.noteId}`, - siteId: subdomain, - tags: page.metadata?.content?.tags - ?.filter((tag) => tag !== "post" && tag !== "page") - ?.join(", "), - applications: page.metadata?.content?.sources, - } - - if (isNotxLogContent) { - return createOrUpdatePage.mutateAsync({ - ...targetNoteBase, - isPost: isPost, // Convert to xLog content - characterId: page.characterId, - }) + if (!page.noteId) { + // Is draft + const key = `draft-${page?.characterId}-${page.draftKey}` + const data = getStorage(key) + data.isPost = !isPost + setStorage(key, data) } else { - if (!page.noteId) { - // Is draft - const key = `draft-${page?.characterId}-${page.draftKey}` - const data = getStorage(key) - data.isPost = !isPost - setStorage(key, data) - } else { - // IsNote - return createOrUpdatePage.mutateAsync({ - ...targetNoteBase, - isPost: !isPost, // Change type - characterId: page.characterId, - }) - } + // IsNote + return updatePage.mutate({ + characterId: page.characterId, + noteId: page.noteId, + isPost: !isPost, // Change type + }) } }), ) - - toast.success(t("Converted!"), { - id: toastId, - }) } catch (e) { // Some failed toast.error(t("Failed to convert."), { diff --git a/src/components/dashboard/PagesManagerMenu.tsx b/src/components/dashboard/PagesManagerMenu.tsx index 322d968e..497f38cc 100644 --- a/src/components/dashboard/PagesManagerMenu.tsx +++ b/src/components/dashboard/PagesManagerMenu.tsx @@ -6,12 +6,11 @@ import { Menu } from "@headlessui/react" import { useQueryClient } from "@tanstack/react-query" import { useGetState } from "~/hooks/useGetState" -import { APP_NAME } from "~/lib/env" import { getNoteSlugFromNote, getTwitterShareUrl } from "~/lib/helpers" import { useTranslation } from "~/lib/i18n/client" import { delStorage, getStorage, setStorage } from "~/lib/storage" import { ExpandedNote } from "~/lib/types" -import { useCreateOrUpdatePage, useDeletePage } from "~/queries/page" +import { useDeletePage, useUpdatePage } from "~/queries/page" import { useGetSite } from "~/queries/site" import { DeleteConfirmationModal } from "./DeleteConfirmationModal" @@ -41,11 +40,10 @@ export const PagesManagerMenu = ({ }) => { const { t } = useTranslation("dashboard") - const isCrossbell = !page.metadata?.content?.sources?.includes("xlog") const router = useRouter() const params = useParams() const subdomain = params?.subdomain as string - const createOrUpdatePage = useCreateOrUpdatePage() + const updatePage = useUpdatePage() const editLink = usePageEditLink(page, isPost) const queryClient = useQueryClient() @@ -76,17 +74,19 @@ export const PagesManagerMenu = ({ }, [deletePage.isError]) useEffect(() => { - if (createOrUpdatePage.isSuccess) { + if (updatePage.isSuccess) { toast.success(t("Converted!"), { id: getCurrentToastId(), }) - } else if (createOrUpdatePage.isError) { + updatePage.reset() + } else if (updatePage.isError) { toast.error(t("Failed to convert."), { id: getCurrentToastId(), }) + updatePage.reset() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [createOrUpdatePage.isSuccess, createOrUpdatePage.isError]) + }, [updatePage.isSuccess, updatePage.isError]) const site = useGetSite(subdomain) @@ -99,60 +99,34 @@ export const PagesManagerMenu = ({ }, }, { - text: - "Convert to " + - (isCrossbell - ? `${APP_NAME} ${isPost ? "Post" : "Page"}` - : isPost - ? "Page" - : "Post"), + text: "Convert to " + (isPost ? "Page" : "Post"), icon: ( ), onClick() { const toastId = toast.loading("Converting...") - if (isCrossbell) { - setConvertToastId(toastId) - createOrUpdatePage.mutate({ - published: true, - pageId: `${page.characterId}-${page.noteId}`, - siteId: subdomain, - tags: page.metadata?.content?.tags - ?.filter((tag) => tag !== "post" && tag !== "page") - ?.join(", "), - isPost: isPost, - applications: page.metadata?.content?.sources, - characterId: page.characterId, + + if (!page.noteId) { + const data = getStorage( + `draft-${site.data?.characterId}-${page.draftKey}`, + ) + data.isPost = !isPost + setStorage(`draft-${site.data?.characterId}-${page.draftKey}`, data) + queryClient.invalidateQueries([ + "getPagesBySite", + site.data?.characterId, + ]) + queryClient.invalidateQueries(["getPage", page.characterId]) + toast.success("Converted!", { + id: toastId, }) } else { - if (!page.noteId) { - const data = getStorage( - `draft-${site.data?.characterId}-${page.draftKey}`, - ) - data.isPost = !isPost - setStorage(`draft-${site.data?.characterId}-${page.draftKey}`, data) - queryClient.invalidateQueries([ - "getPagesBySite", - site.data?.characterId, - ]) - queryClient.invalidateQueries(["getPage", page.characterId]) - toast.success("Converted!", { - id: toastId, - }) - } else { - setConvertToastId(toastId) - createOrUpdatePage.mutate({ - published: true, - pageId: `${page.characterId}-${page.noteId}`, - siteId: subdomain, - tags: page.metadata?.content?.tags - ?.filter((tag) => tag !== "post" && tag !== "page") - ?.join(", "), - isPost: !isPost, - applications: page.metadata?.content?.sources, - characterId: page.characterId, - }) - } + setConvertToastId(toastId) + updatePage.mutate({ + isPost: !isPost, + characterId: page.characterId, + noteId: page.noteId, + }) } }, }, diff --git a/src/models/page.model.ts b/src/models/page.model.ts index 5b4699a7..f6ca83ce 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -43,115 +43,6 @@ export async function checkPageSlug(input: { } } -export async function createOrUpdatePage( - input: { - pageId?: string - siteId: string - slug?: string - tags?: string - title?: string - content?: string - published?: boolean - publishedAt?: string - excerpt?: string - /** Only needed when creating page */ - isPost?: boolean - applications?: string[] - cover?: { - address?: string - mime_type?: string - } - disableAISummary?: boolean - }, - customUnidata?: Unidata, - newbieToken?: string, -) { - const { default: unidata } = await import("~/queries/unidata.server") - - if (!input.published) { - return await (customUnidata || unidata).notes.set( - { - source: "Crossbell Note", - identity: input.siteId, - platform: "Crossbell", - action: "remove", - }, - { - id: input.pageId, - }, - { - newbieToken, - }, - ) - } - return await (customUnidata || unidata).notes.set( - { - source: "Crossbell Note", - identity: input.siteId, - platform: "Crossbell", - action: input.pageId ? "update" : "add", - }, - { - ...(input.pageId && { id: input.pageId }), - ...(input.title && { title: input.title }), - ...(input.content && { - body: { - content: input.content, - mime_type: "text/markdown", - }, - }), - date_published: input.publishedAt || new Date().toISOString(), - ...(input.excerpt && { - summary: { - content: input.excerpt, - mime_type: "text/markdown", - }, - }), - tags: [ - input.isPost ? "post" : "page", - ...(input.tags - ?.split(",") - .map((tag) => tag.trim()) - .filter((tag) => tag) || []), - ], - applications: [ - "xlog", - ...(input.applications?.filter((app) => app !== "xlog") || []), - ], - attributes: [ - ...(input.slug - ? [ - { - trait_type: "xlog_slug", - value: input.slug, - }, - ] - : []), - ...(input.disableAISummary - ? [ - { - trait_type: "xlog_disable_ai_summary", - value: input.disableAISummary, - }, - ] - : []), - ], - attachments: input.cover?.address - ? [ - { - name: "cover", - address: input.cover.address, - mime_type: input.cover.mime_type, - }, - ] - : [], - }, - { - newbieToken, - }, - ) -} - export async function postNotes( input: { siteId: string diff --git a/src/queries/page.ts b/src/queries/page.ts index dc96d863..63c9732f 100644 --- a/src/queries/page.ts +++ b/src/queries/page.ts @@ -1,13 +1,18 @@ "use client" +import { NoteMetadata } from "crossbell" +import { nanoid } from "nanoid" + import { useAccountState, useIsNoteLiked, useMintNote, useNoteLikeCount, useNoteLikeList, + usePostNote, usePostNoteForNote, useToggleLikeNote, + useUpdateNote, } from "@crossbell/connect-kit" import { useContract } from "@crossbell/contract" import { useRefCallback } from "@crossbell/util-hooks" @@ -202,26 +207,211 @@ export const useCheckComment = ({ ) } -export function useCreateOrUpdatePage() { - const newbieToken = useAccountState((s) => s.email?.token) - const unidata = useUnidata() +export function useCreatePage() { const queryClient = useQueryClient() - const mutation = useMutation( - async ( - payload: Parameters[0] & { - characterId?: number - }, - ) => { - return pageModel.createOrUpdatePage(payload, unidata, newbieToken) - }, - { - onSuccess: (data, variables) => { - queryClient.invalidateQueries(["getPagesBySite", variables.characterId]) - queryClient.invalidateQueries(["getPage", variables.characterId]) - }, + const { mutateAsync: _, ...postNote } = usePostNote() + + const mutate = useRefCallback( + (input: { + characterId?: number + slug?: string + tags?: string + title?: string + content?: string + publishedAt?: string + excerpt?: string + isPost?: boolean + cover?: { + address?: string + mime_type?: string + } + disableAISummary?: boolean + }) => { + if (!input.characterId) { + throw new Error("characterId is required") + } + + return postNote.mutate( + { + characterId: input.characterId, + metadata: { + title: input.title, + content: input.content, + date_published: input.publishedAt || new Date().toISOString(), + summary: input.excerpt, + tags: [ + input.isPost ? "post" : "page", + ...(input.tags + ?.split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag) || []), + ], + sources: ["xlog"], + attributes: [ + { + trait_type: "xlog_slug", + value: input.slug || nanoid(), + }, + ...(input.disableAISummary + ? [ + { + trait_type: "xlog_disable_ai_summary", + value: input.disableAISummary, + }, + ] + : []), + ], + attachments: [ + ...(input.cover?.address + ? [ + { + name: "cover", + address: input.cover.address, + mime_type: input.cover.mime_type, + }, + ] + : []), + ], + } as NoteMetadata & { + summary?: string + }, + }, + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries(["getPagesBySite", input.characterId]) + queryClient.invalidateQueries(["getPage", input.characterId]) + }, + }, + ) }, ) - return mutation + + return { + ...postNote, + mutate, + } +} + +export function useUpdatePage() { + const queryClient = useQueryClient() + const { mutateAsync: _, ...updateNote } = useUpdateNote() + + const mutate = useRefCallback( + (input: { + noteId?: number + characterId?: number + slug?: string + tags?: string + title?: string + content?: string + publishedAt?: string + excerpt?: string + isPost?: boolean + cover?: { + address?: string + mime_type?: string + } + disableAISummary?: boolean + }) => { + if (!input.characterId || !input.noteId) { + throw new Error("characterId and noteId are required") + } + + return updateNote.mutate( + { + note: { + characterId: input.characterId, + noteId: input.noteId, + }, + edit(metadataDraft) { + if (input.title !== undefined) { + metadataDraft.title = input.title + } + if (input.content !== undefined) { + metadataDraft.content = input.content + } + if (input.publishedAt !== undefined) { + metadataDraft.date_published = input.publishedAt + } + if (input.excerpt !== undefined) { + ;(metadataDraft as any).summary = input.excerpt + } + + if (!metadataDraft.tags) { + metadataDraft.tags = [] + } + if (input.isPost !== undefined) { + metadataDraft.tags[0] = input.isPost ? "post" : "page" + } + if (input.tags !== undefined) { + metadataDraft.tags = [ + metadataDraft.tags[0], + ...input.tags + .split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag), + ] + } + + if (input.slug !== undefined) { + const slug = metadataDraft.attributes?.find( + (attr) => attr.trait_type === "xlog_slug", + ) + if (slug) { + slug.value = input.slug + } else { + metadataDraft.attributes?.push({ + trait_type: "xlog_slug", + value: input.slug, + }) + } + } + + if (input.disableAISummary !== undefined) { + const disableAISummary = metadataDraft.attributes?.find( + (attr) => attr.trait_type === "xlog_disable_ai_summary", + ) + if (disableAISummary) { + disableAISummary.value = input.disableAISummary + } else { + metadataDraft.attributes?.push({ + trait_type: "xlog_disable_ai_summary", + value: input.disableAISummary, + }) + } + } + + if (input.cover?.address !== undefined) { + const cover = metadataDraft.attachments?.find( + (attr) => attr.name === "cover", + ) + if (cover) { + cover.address = input.cover.address + cover.mime_type = input.cover.mime_type + } else { + metadataDraft.attachments?.push({ + name: "cover", + address: input.cover.address, + mime_type: input.cover.mime_type, + }) + } + } + }, + }, + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries(["getPagesBySite", input.characterId]) + queryClient.invalidateQueries(["getPage", input.characterId]) + }, + }, + ) + }, + ) + + return { + ...updateNote, + mutate, + } } export function usePostNotes() {