feat: crossbell renovation - editor page
This commit is contained in:
parent
fc5c57f7d8
commit
877cf5be96
|
|
@ -27,3 +27,5 @@ MAILGUN_DOMAIN_NEWSLETTER=
|
|||
|
||||
# access token for our umami instance
|
||||
UMAMI_TOKEN=
|
||||
|
||||
WEB3_STORAGE_API_TOKEN=
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 || [])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<UploadFile>(
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Note | undefined>(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<string>('')
|
||||
|
||||
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 (
|
||||
<>
|
||||
<DashboardLayout title="Editor">
|
||||
|
|
@ -270,37 +238,6 @@ export default function SubdomainEditor() {
|
|||
} will be accesisble from this time`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="slug"
|
||||
value={values.slug}
|
||||
label="Page slug"
|
||||
id="slug"
|
||||
isBlock
|
||||
placeholder="some-slug"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateValue("slug", e.target.value)
|
||||
}
|
||||
help={
|
||||
<>
|
||||
{values.slug && (
|
||||
<>
|
||||
This {isPost ? "post" : "page"} will be accessible at{" "}
|
||||
<UniLink
|
||||
href={`${getSiteLink({ subdomain })}/${
|
||||
values.slug
|
||||
}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{getSiteLink({ subdomain, noProtocol: true })}/
|
||||
{values.slug}
|
||||
</UniLink>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label="Excerpt"
|
||||
|
|
@ -316,35 +253,6 @@ export default function SubdomainEditor() {
|
|||
help="Leave it blank to use auto-generated excerpt"
|
||||
/>
|
||||
</div>
|
||||
{isPost && page && (
|
||||
<div>
|
||||
<FieldLabel label="Email Post" id="email-post" />
|
||||
{page?.emailStatus ? (
|
||||
<div className="capitalize text-zinc-500 text-sm flex items-center space-x-1">
|
||||
<span
|
||||
className={clsx(
|
||||
page.emailStatus === "SUCCESS"
|
||||
? `i-bi:check-circle text-green-500`
|
||||
: page.emailStatus === "FAILED"
|
||||
? `i-bi:x-circle text-red-500`
|
||||
: ``,
|
||||
)}
|
||||
></span>
|
||||
<span>{page.emailStatus.toLowerCase()}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
variantColor="gray"
|
||||
size="sm"
|
||||
isBlock
|
||||
onClick={() => setEmailPostModalOpened(true)}
|
||||
>
|
||||
Open Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardMain>
|
||||
|
|
|
|||
Loading…
Reference in New Issue