feat: crossbell renovation - editor page

This commit is contained in:
DIYgod 2022-06-24 23:39:03 +01:00
parent fc5c57f7d8
commit 877cf5be96
No known key found for this signature in database
GPG Key ID: D159328F47A80DCA
9 changed files with 131 additions and 236 deletions

View File

@ -27,3 +27,5 @@ MAILGUN_DOMAIN_NEWSLETTER=
# access token for our umami instance
UMAMI_TOKEN=
WEB3_STORAGE_API_TOKEN=

View File

@ -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"
},

View File

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

View File

@ -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 || [])
})

View File

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

View File

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

View File

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

View File

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

View File

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