feat: use crossbell kit to create and update page

This commit is contained in:
DIYgod 2023-06-14 22:58:47 +01:00
parent b23fe136cd
commit 98893e661d
No known key found for this signature in database
7 changed files with 329 additions and 275 deletions

View File

@ -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() {
<OptionsButton
visibility={visibility}
savePage={savePage}
deletePage={deletePage}
published={visibility !== PageVisibilityEnum.Draft}
isRendering={isRendering}
renderPage={setIsRendering}
@ -539,7 +572,7 @@ export default function SubdomainEditor() {
: ""
}
published={visibility !== PageVisibilityEnum.Draft}
isSaving={createOrUpdatePage.isLoading}
isSaving={createPage.isLoading || updatePage.isLoading}
isDisabled={
visibility !== PageVisibilityEnum.Modified &&
visibility !== PageVisibilityEnum.Draft

View File

@ -12,6 +12,7 @@ export const OptionsButton = ({
visibility,
previewPage,
savePage,
deletePage,
isRendering,
renderPage,
published,
@ -23,7 +24,8 @@ export const OptionsButton = ({
visibility: PageVisibilityEnum | undefined
previewPage: () => void
renderPage: Dispatch<SetStateAction<boolean>>
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")}
</button>
@ -152,7 +154,7 @@ export const OptionsButton = ({
open={deleteConfirmModalOpen}
setOpen={setDeleteConfirmModalOpen}
onConfirm={() => {
savePage(false)
deletePage()
}}
isPost={isPost}
/>

View File

@ -222,7 +222,6 @@ export const PagesManager = ({ isPost }: { isPost: boolean }) => {
{batchSelected.length > 0 ? (
<PagesManagerBatchSelectActionTab
isPost={isPost}
isNotxLogContent={false}
pages={pages.data}
batchSelected={batchSelected}
setBatchSelected={setBatchSelected}

View File

@ -6,11 +6,10 @@ import type { InfiniteData } from "@tanstack/react-query"
import { useQueryClient } from "@tanstack/react-query"
import { type TabItem, Tabs } from "~/components/ui/Tabs"
import { APP_NAME } from "~/lib/env"
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 { DeleteConfirmationModal } from "./DeleteConfirmationModal"
@ -20,13 +19,11 @@ function getPageId(page: ExpandedNote) {
export const PagesManagerBatchSelectActionTab = ({
isPost,
isNotxLogContent,
pages,
batchSelected,
setBatchSelected,
}: {
isPost: boolean
isNotxLogContent: boolean
pages?: InfiniteData<{
list: ExpandedNote[]
}>
@ -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."), {

View File

@ -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: (
<span className="icon-[mingcute--transfer-3-line] inline-block"></span>
),
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,
})
}
},
},

View File

@ -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

View File

@ -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<typeof pageModel.createOrUpdatePage>[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() {