From 877cf5be96d70cb1cbcc5a55e1d49ecb19fb2e03 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 24 Jun 2022 23:39:03 +0100 Subject: [PATCH] feat: crossbell renovation - editor page --- .env.example | 2 + package.json | 3 +- pnpm-lock.yaml | 35 ++++- src/components/dashboard/PagesManager.tsx | 2 + src/hooks/useUploadFile.ts | 34 ++-- src/lib/env.ts | 1 + src/lib/page-helpers.ts | 8 +- src/models/page.model.ts | 108 ++++--------- src/pages/dashboard/[subdomain]/editor.tsx | 174 +++++---------------- 9 files changed, 131 insertions(+), 236 deletions(-) diff --git a/.env.example b/.env.example index 0369a293..97cae81c 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,5 @@ MAILGUN_DOMAIN_NEWSLETTER= # access token for our umami instance UMAMI_TOKEN= + +WEB3_STORAGE_API_TOKEN= diff --git a/package.json b/package.json index a07091e7..ab956ba3 100644 --- a/package.json +++ b/package.json @@ -63,13 +63,14 @@ "remark-rehype": "^10.1.0", "sortablejs": "^1.15.0", "ua-parser-js": "^1.0.2", - "unidata.js": "^0.5.9", + "unidata.js": "^0.5.10", "unified": "^10.1.2", "unist-builder": "^3.0.0", "unist-util-visit": "^4.1.0", "use-onclickoutside": "^0.4.0", "uuid": "^8.3.2", "wagmi": "^0.4.12", + "web3.storage": "^4.3.0", "zod": "^3.15.1", "zustand": "4.0.0-rc.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16ba92b4..9e5ad550 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,7 +80,7 @@ specifiers: tailwindcss: ^3.0.24 typescript: ^4.5.5 ua-parser-js: ^1.0.2 - unidata.js: ^0.5.9 + unidata.js: ^0.5.10 unified: ^10.1.2 unist-builder: ^3.0.0 unist-util-visit: ^4.1.0 @@ -89,6 +89,7 @@ specifiers: uuid: ^8.3.2 vitest: ^0.12.9 wagmi: ^0.4.12 + web3.storage: ^4.3.0 zod: ^3.15.1 zustand: 4.0.0-rc.1 @@ -141,13 +142,14 @@ dependencies: remark-rehype: 10.1.0 sortablejs: 1.15.0 ua-parser-js: 1.0.2 - unidata.js: 0.5.9 + unidata.js: 0.5.10 unified: 10.1.2 unist-builder: 3.0.0 unist-util-visit: 4.1.0 use-onclickoutside: 0.4.1_luyos4mouogwq6z3wafb3re4ce uuid: 8.3.2 wagmi: 0.4.12_um4cugg2i3ywhvigsmvsir7hgy + web3.storage: 4.3.0 zod: 3.17.3 zustand: 4.0.0-rc.1_react@18.2.0 @@ -7662,8 +7664,8 @@ packages: resolution: {integrity: sha512-MEvryPLf18HvlCbLSzCW0U00IMftKGI5udnjrQbC5D4P0Hodwffhv+iGfWuJwg16Y/TK11ZFK8i+BPVW2z/eAw==} engines: {node: '>=12.18'} - /unidata.js/0.5.9: - resolution: {integrity: sha512-acX2t+ndj1Je3zTMWfjyDKQXljdhzDkdZXPqIUS9qqmRdAIavgOJJsCR68ukPh6aNg6hZWPsO5TbgLrSdNuK9g==} + /unidata.js/0.5.10: + resolution: {integrity: sha512-/h4Y42AknD06/NnhDbduWqjn3OySIAAox7hzBBpZLPkai5AezVx3cS5GTUEB2S5taVZgC12upSUDXJPPtQutuA==} dependencies: '@types/lodash': 4.14.182 '@urql/core': 2.5.0_graphql@16.5.0 @@ -8129,6 +8131,31 @@ packages: - supports-color dev: false + /web3.storage/4.3.0: + resolution: {integrity: sha512-9IKn+4dyF7cMXC9etVzW3qmCV/6sL+Za4kcUUn7D3qRt4s1DrKUGJdAe9VvYcpHwbnnYoKNBu06lGO++daDo8w==} + dependencies: + '@ipld/car': 3.2.4 + '@web-std/blob': 3.0.4 + '@web-std/fetch': 3.0.3 + '@web-std/file': 3.0.2 + '@web3-storage/parse-link-header': 3.1.0 + browser-readablestream-to-it: 1.0.3 + carbites: 1.0.6 + cborg: 1.9.4 + files-from-path: 0.2.6 + ipfs-car: 0.7.0 + ipns: 0.16.0 + libp2p-crypto: 0.21.2 + p-retry: 4.6.2 + streaming-iterables: 6.2.0 + throttled-queue: 2.1.4 + uint8arrays: 3.0.0 + transitivePeerDependencies: + - encoding + - node-fetch + - supports-color + dev: false + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false diff --git a/src/components/dashboard/PagesManager.tsx b/src/components/dashboard/PagesManager.tsx index e7f54e39..b1945ea2 100644 --- a/src/components/dashboard/PagesManager.tsx +++ b/src/components/dashboard/PagesManager.tsx @@ -41,6 +41,7 @@ export const PagesManager: React.FC<{ site: subdomain, take: 1000, visibility, + render: true, }).then((pages) => { setPages(pages?.list || []) }) @@ -54,6 +55,7 @@ export const PagesManager: React.FC<{ site: subdomain!, take: 1000, visibility, + render: true, }).then((pages) => { setPages(pages?.list || []) }) diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts index 9fb499df..91b1bec6 100644 --- a/src/hooks/useUploadFile.ts +++ b/src/hooks/useUploadFile.ts @@ -1,30 +1,24 @@ import { useCallback } from "react" -import { R2_URL } from "~/lib/env" -import { trpc } from "~/lib/trpc" +import { WEB3_STORAGE_API_TOKEN } from "~/lib/env" +import { Web3Storage } from 'web3.storage' export const useUploadFile = () => { - const { fetchQuery } = trpc.useContext() - const uploadFile = useCallback( async (blob, filename) => { - const jwt = await fetchQuery(["user.getSignedJwt"], {}) - - const form = new FormData() - form.append("file", blob, filename) - const res = await fetch(R2_URL, { - body: form, - method: "post", - headers: { - authorization: `Bearer ${jwt}`, - }, - }) - if (!res.ok) { - throw new Error(await res.text()) + const file = new File([blob], filename); + const web3Storage = new Web3Storage({ + token: WEB3_STORAGE_API_TOKEN, + } as any) + const cid = await web3Storage.put([file], { + name: file.name, + maxRetries: 3, + wrapWithDirectory: false, + }); + return { + key: `https://gateway.ipfs.io/ipfs/${cid}` } - const data = await res.json() - return data }, - [fetchQuery] + [] ) return uploadFile diff --git a/src/lib/env.ts b/src/lib/env.ts index da10bfd4..4d1e92ae 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -8,3 +8,4 @@ export const R2_URL = process.env.NEXT_PUBLIC_R2_URL export const APP_DESCRIPTION = process.env.APP_DESCRIPTION export const DISCORD_LINK = process.env.NEXT_PUBLIC_DISCORD_LINK export const GITHUB_LINK = process.env.NEXT_PUBLIC_GITHUB_LINK +export const WEB3_STORAGE_API_TOKEN = process.env.WEB3_STORAGE_API_TOKEN \ No newline at end of file diff --git a/src/lib/page-helpers.ts b/src/lib/page-helpers.ts index f38dc250..971d3a90 100644 --- a/src/lib/page-helpers.ts +++ b/src/lib/page-helpers.ts @@ -19,9 +19,11 @@ export const usePageVisibility = ({ export const getPageVisibility = ({ date_published, }: { - date_published: string -}) => { - if (dayjs(date_published).isBefore(new Date())) { + date_published?: string + }) => { + if (!date_published) { + return PageVisibilityEnum.Draft + } else if (dayjs(date_published).isBefore(new Date())) { return PageVisibilityEnum.Published } else if (dayjs(date_published).isBefore(new Date('9999-01-01'))) { return PageVisibilityEnum.Scheduled diff --git a/src/models/page.model.ts b/src/models/page.model.ts index 76ea84d7..9e8bd762 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -54,11 +54,9 @@ const checkPageSlug = async ({ } export async function createOrUpdatePage( - gate: Gate, input: { pageId?: string siteId: string - slug?: string title?: string content?: string published?: boolean @@ -68,77 +66,35 @@ export async function createOrUpdatePage( isPost?: boolean }, ) { - // const user = gate.getUser(true) - // const page = input.pageId - // ? await prismaPrimary.page.findUnique({ - // where: { - // id: input.pageId, - // }, - // include: { - // site: true, - // }, - // }) - // : await prismaPrimary.page.create({ - // data: { - // title: "Untitled", - // slug: `untitled-${nanoid(4)}`, - // site: { - // connect: { - // id: input.siteId, - // }, - // }, - // content: "", - // excerpt: "", - // authors: { - // connect: { - // id: user.id, - // }, - // }, - // }, - // include: { - // site: true, - // }, - // }) - - // if (!page || page.deletedAt) { - // throw new Error(`Page not found`) - // } - - // if (input.pageId) { - // if (!gate.allows({ type: "can-update-page", siteId: page.siteId })) { - // throw gate.permissionError() - // } - // } else { - // if (!gate.allows({ type: "can-create-page", siteId: page.siteId })) { - // throw gate.permissionError() - // } - // } - - // const slug = input.slug || page.slug - // await checkPageSlug({ slug, excludePage: page.id, siteId: page.siteId }) - - // // Just checking if the page content can be rendered - // const rendered = input.content - // ? await renderPageContent(input.content) - // : undefined - - // const updated = await prismaPrimary.page.update({ - // where: { - // id: page.id, - // }, - // data: { - // title: input.title || "Untitled", - // content: input.content, - // published: input.published, - // publishedAt: input.publishedAt && new Date(input.publishedAt), - // excerpt: input.excerpt && stripHTML(input.excerpt), - // slug, - // type: input.isPost ? "POST" : "PAGE", - // rendered, - // }, - // }) - - // return { page: updated } + if (unidata) { + return await unidata.notes.set({ + source: 'Crossbell Note', + identity: input.siteId, + platform: 'Crossbell', + action: input.pageId ? 'add' : 'update', + }, { + ...(input.pageId && { id: input.pageId }), + ...(input.title && { title: input.title }), + ...(input.content && { + body: { + content: input.content, + mime_type: 'text/markdown', + }}), + ...(input.publishedAt && { date_published: input.published ? input.publishedAt : new Date('9999-01-01').toISOString() }), + ...(input.excerpt && { + summary: { + content: input.excerpt, + mime_type: 'text/markdown', + } + }), + tags: [input.isPost ? "post" : "page"], + }) + } else { + return { + code: 1, + message: 'Unidata is not found', + } + } } export async function scheduleEmailForPost( @@ -179,6 +135,7 @@ export async function getPagesBySite( cursor?: string | null includeContent?: boolean includeExcerpt?: boolean + render?: boolean }, ) { if (!input.site) { @@ -188,7 +145,7 @@ export async function getPagesBySite( } } - const visibility = input.visibility || PageVisibilityEnum.Published + const visibility = input.visibility || PageVisibilityEnum.All let pages: Notes | null = null if (unidata) { @@ -198,6 +155,7 @@ export async function getPagesBySite( platform: 'Crossbell', limit: input.take || 100, }); + console.log(pages?.list.length, visibility) if (pages?.list) { switch (visibility) { @@ -214,7 +172,7 @@ export async function getPagesBySite( pages.total = pages.list.length pages.list = await Promise.all(pages?.list.map(async (page) => { - if (page.body?.content && page.body?.mime_type === 'text/markdown') { + if (page.body?.content && page.body?.mime_type === 'text/markdown' && input.render) { const rendered = await renderPageContent(page.body.content) page.body = { content: rendered.contentHTML, diff --git a/src/pages/dashboard/[subdomain]/editor.tsx b/src/pages/dashboard/[subdomain]/editor.tsx index 3ee2095f..ed1a1b1a 100644 --- a/src/pages/dashboard/[subdomain]/editor.tsx +++ b/src/pages/dashboard/[subdomain]/editor.tsx @@ -17,12 +17,13 @@ import { useUploadFile } from "~/hooks/useUploadFile" import { inLocalTimezone } from "~/lib/date" import { getSiteLink } from "~/lib/helpers" import { getPageVisibility } from "~/lib/page-helpers" -import { trpc } from "~/lib/trpc" import { PageVisibilityEnum } from "~/lib/types" import { FieldLabel } from "~/components/ui/FieldLabel" import { Button } from "~/components/ui/Button" import { EmailPostModal } from "~/components/common/EmailPostModal" import { useStore } from "~/lib/store" +import type { Note } from "unidata.js" +import { getPagesBySite, createOrUpdatePage } from "~/models/page.model" const getInputDatetimeValue = (date: Date | string) => { const str = dayjs(date).format() @@ -35,41 +36,39 @@ export default function SubdomainEditor() { const subdomain = router.query.subdomain as string const isPost = router.query.type === "post" const pageId = router.query.id as string | undefined - const trpcContext = trpc.useContext() - const siteResult = trpc.useQuery(["site", { site: subdomain }], { - enabled: !!subdomain, - }) - const { data: page, refetch: refetchPage } = trpc.useQuery( - ["site.page", { page: pageId!, site: subdomain, render: false }], - { - enabled: !!pageId, - }, - ) - const pageContent = useMemo(() => page?.content, [page?.content]) - const published = page?.published ?? false + let [page, setPage] = useState(undefined) + + useEffect(() => { + if (subdomain) { + getPagesBySite({ + type: isPost ? "post" : "page", + site: subdomain!, + take: 1000, + render: false + }).then((pages) => { + console.log(pages, pageId) + const page = (pages?.list || []).find((p) => p.id === pageId) + setPage(page) + }) + } + }, [subdomain, isPost, pageId]) + + const pageContent = useMemo(() => page?.body?.content, [page?.body?.content]) + const visibility = getPageVisibility({ - published, - publishedAt: page?.publishedAt || null, + date_published: page?.date_published, }) - const { - mutate: createOrUpdatePage, - status: createOrUpdatePageStatus, - data: createOrUpdatePageResult, - error: createOrUpdatePageError, - reset: resetCreateOrUpdatePage, - } = trpc.useMutation("page.createOrUpdate") + const published = visibility !== PageVisibilityEnum.Draft + + const [createOrUpdatePageStatus, setCreateOrUpdatePageStatus] = useState('') + const uploadFile = useUploadFile() - const [emailPostModalOpened, setEmailPostModalOpened] = useStore((store) => [ - store.emailPostModalOpened, - store.setEmailPostModalOpened, - ]) const [values, setValues] = useState({ title: "", publishedAt: new Date().toISOString(), published: false, - slug: "", excerpt: "", }) const [content, setContent] = useState("") @@ -89,13 +88,22 @@ export default function SubdomainEditor() { ) const savePage = (published: boolean) => { + setCreateOrUpdatePageStatus("loading") createOrUpdatePage({ ...values, - siteId: siteResult.data!.id, - pageId: page?.id, + 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) + } }) } @@ -136,48 +144,14 @@ export default function SubdomainEditor() { placeholder: `Start writing...`, }) - useEffect(() => { - if (createOrUpdatePageStatus === "success") { - resetCreateOrUpdatePage() - toast.success(values.published ? "Updated" : "Saved!") - trpcContext.invalidateQueries("site.page") - router.replace( - `/dashboard/${subdomain}/editor?id=${ - createOrUpdatePageResult.id - }&type=${isPost ? "post" : "page"}`, - ) - } - }, [ - resetCreateOrUpdatePage, - createOrUpdatePageStatus, - createOrUpdatePageResult, - isPost, - router, - subdomain, - trpcContext, - values.published, - ]) - - useEffect(() => { - if (createOrUpdatePageStatus === "error") { - resetCreateOrUpdatePage() - toast.error(createOrUpdatePageError.message) - } - }, [ - resetCreateOrUpdatePage, - createOrUpdatePageStatus, - createOrUpdatePageError, - ]) - useEffect(() => { if (!page) return setValues({ - title: page.title, - publishedAt: page.publishedAt, - published: page.published, - slug: page.slug, - excerpt: page.excerpt || "", + title: page.title || '', + publishedAt: page.date_published, + published: page.date_published !== new Date('9999-01-01').toISOString(), + excerpt: page.summary?.content || "", }) }, [page]) @@ -187,12 +161,6 @@ export default function SubdomainEditor() { } }, [pageContent]) - useEffect(() => { - if (!emailPostModalOpened) { - refetchPage() - } - }, [refetchPage, emailPostModalOpened]) - return ( <> @@ -270,37 +238,6 @@ export default function SubdomainEditor() { } will be accesisble from this time`} /> -
- ) => - updateValue("slug", e.target.value) - } - help={ - <> - {values.slug && ( - <> - This {isPost ? "post" : "page"} will be accessible at{" "} - - {getSiteLink({ subdomain, noProtocol: true })}/ - {values.slug} - - - )} - - } - /> -
- {isPost && page && ( -
- - {page?.emailStatus ? ( -
- - {page.emailStatus.toLowerCase()} -
- ) : ( - - )} -
- )}