From 697ee50cf1c91d15ed1bd99a6a1b7b01d2e241d8 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 25 Aug 2023 03:03:05 +0100 Subject: [PATCH] feat: use unified editor properties --- .../[subdomain]/editor/portfolio-editor.tsx | 216 ++++----------- .../[subdomain]/editor/post-editor.tsx | 257 ++++-------------- .../editor-properties/EditorAutofill.tsx | 43 +++ .../editor-properties/EditorCover.tsx | 39 +++ .../EditorDisableAISummary.tsx | 27 ++ .../editor-properties/EditorExcerpt.tsx | 36 +++ .../editor-properties/EditorExternalUrl.tsx | 31 +++ .../editor-properties/EditorPublishAt.tsx | 61 +++++ .../editor-properties/EditorSlug.tsx | 56 ++++ .../editor-properties/EditorTags.tsx | 41 +++ .../editor-properties/EditorTitle.tsx | 31 +++ src/lib/i18n/locales/zh/dashboard.json | 3 +- 12 files changed, 482 insertions(+), 359 deletions(-) create mode 100644 src/components/dashboard/editor-properties/EditorAutofill.tsx create mode 100644 src/components/dashboard/editor-properties/EditorCover.tsx create mode 100644 src/components/dashboard/editor-properties/EditorDisableAISummary.tsx create mode 100644 src/components/dashboard/editor-properties/EditorExcerpt.tsx create mode 100644 src/components/dashboard/editor-properties/EditorExternalUrl.tsx create mode 100644 src/components/dashboard/editor-properties/EditorPublishAt.tsx create mode 100644 src/components/dashboard/editor-properties/EditorSlug.tsx create mode 100644 src/components/dashboard/editor-properties/EditorTags.tsx create mode 100644 src/components/dashboard/editor-properties/EditorTitle.tsx diff --git a/src/app/dashboard/[subdomain]/editor/portfolio-editor.tsx b/src/app/dashboard/[subdomain]/editor/portfolio-editor.tsx index b590a2cc..2a5e22e0 100644 --- a/src/app/dashboard/[subdomain]/editor/portfolio-editor.tsx +++ b/src/app/dashboard/[subdomain]/editor/portfolio-editor.tsx @@ -2,28 +2,34 @@ import { nanoid } from "nanoid" import { useParams, useRouter, useSearchParams } from "next/navigation" -import { ChangeEvent, memo, useCallback, useEffect, useState } from "react" +import { memo, useCallback, useEffect, useState } from "react" import toast from "react-hot-toast" -import { DateInput } from "@mantine/dates" import { useQueryClient } from "@tanstack/react-query" import { DashboardMain } from "~/components/dashboard/DashboardMain" import { PublishButton } from "~/components/dashboard/PublishButton" import PublishedModal from "~/components/dashboard/PublishedModal" -import { Button } from "~/components/ui/Button" -import { FieldLabel } from "~/components/ui/FieldLabel" -import { ImageUploader } from "~/components/ui/ImageUploader" -import { Input } from "~/components/ui/Input" +import EditorAutofill from "~/components/dashboard/editor-properties/EditorAutofill" +import EditorCover from "~/components/dashboard/editor-properties/EditorCover" +import EditorExcerpt from "~/components/dashboard/editor-properties/EditorExcerpt" +import EditorExternalUrl from "~/components/dashboard/editor-properties/EditorExternalUrl" +import EditorPublishAt from "~/components/dashboard/editor-properties/EditorPublishAt" +import EditorTitle from "~/components/dashboard/editor-properties/EditorTitle" import { useModalStack } from "~/components/ui/ModalStack" -import { initialEditorState, useEditorState } from "~/hooks/useEditorState" +import { + Values, + initialEditorState, + useEditorState, +} from "~/hooks/useEditorState" +import { useGetState } from "~/hooks/useGetState" import { useBeforeMounted } from "~/hooks/useSyncOnce" import { showConfetti } from "~/lib/confetti" import { CSB_SCAN } from "~/lib/env" import { getTwitterShareUrl } from "~/lib/helpers" import { useTranslation } from "~/lib/i18n/client" import { getPageVisibility } from "~/lib/page-helpers" -import { delStorage } from "~/lib/storage" +import { delStorage, setStorage } from "~/lib/storage" import { PageVisibilityEnum } from "~/lib/types" import { cn } from "~/lib/utils" import { @@ -100,6 +106,31 @@ export default function PortfolioEditor() { const values = useEditorState() + const getValues = useGetState(values) + const updateValue = useCallback( + (val: Partial) => { + if (visibility !== PageVisibilityEnum.Draft) { + setVisibility(PageVisibilityEnum.Modified) + } + + const values = getValues() + const newValues = { ...values, ...val } + if (draftKey) { + setStorage(draftKey, { + date: +new Date(), + values: newValues, + type, + }) + queryClient.invalidateQueries([ + "getPagesBySite", + site.data?.characterId, + ]) + } + useEditorState.setState(newValues) + }, + [visibility], + ) + const createPage = useCreatePage() const updatePage = useUpdatePage() @@ -233,7 +264,7 @@ export default function PortfolioEditor() { ) : ( <> - +
{ - const values = useEditorState() - const { t } = useTranslation("dashboard") - const updateValue = useEditorState.setState +const EditorExtraProperties = memo( + ({ updateValue }: { updateValue: (val: Partial) => void }) => { + const { t } = useTranslation("dashboard") - const [filling, setFilling] = useState(false) - const autofill = async () => { - setFilling(true) - const result = await ( - await fetch(`/api/open-graph?url=${values.externalUrl}`) - ).json() - const time = - result?.articlePublishedTime || result?.publishedTime || result?.ogDate - - updateValue({ - cover: result?.ogImage?.[0] - ? { - address: result?.ogImage?.[0]?.url, - mime_type: result?.ogImage?.[0]?.type, - } - : undefined, - title: result?.ogTitle, - excerpt: result?.ogDescription, - publishedAt: time ? new Date(time).toISOString() : undefined, - }) - setFilling(false) - } - - return ( -
-
- ) => { - updateValue({ - externalUrl: e.target.value, - }) - }} - /> -
- -
- - { - const { address, mime_type } = key as { - address?: string - mime_type?: string - } - updateValue({ - cover: { - address, - mime_type, - }, - }) - }} - accept="image/*" - /> -
-
- ) => { - updateValue({ - title: e.target.value, - }) - }} - /> -
-
- ) => { - updateValue({ - excerpt: e.target.value, - }) - }} - /> -
-
- - { - if (value) { - updateValue({ - publishedAt: value.toISOString(), - }) - } else { - updateValue({ - publishedAt: "", - }) - } - }} - styles={{ - input: { - borderRadius: "0.5rem", - borderColor: "var(--border-color)", - height: "2.5rem", - "&:focus-within": { - borderColor: "var(--theme-color)", - }, - }, - }} - /> -
- {t( - `This portfolio will be accessible from this time. Leave blank to use the current time.`, - )} + return ( +
+ + +
+
- {values.publishedAt > new Date().toISOString() && ( -
- {t( - "The post is currently not public as its publication date has been scheduled for a future time.", - )} -
- )} + + +
-
- ) -}) + ) + }, +) EditorExtraProperties.displayName = "EditorExtraProperties" diff --git a/src/app/dashboard/[subdomain]/editor/post-editor.tsx b/src/app/dashboard/[subdomain]/editor/post-editor.tsx index a786f0e2..431eb8d4 100644 --- a/src/app/dashboard/[subdomain]/editor/post-editor.tsx +++ b/src/app/dashboard/[subdomain]/editor/post-editor.tsx @@ -2,12 +2,10 @@ import { nanoid } from "nanoid" import { useParams, useRouter, useSearchParams } from "next/navigation" -import { ChangeEvent, memo, useCallback, useEffect, useState } from "react" +import { memo, useCallback, useEffect, useState } from "react" import toast from "react-hot-toast" -import { shallow } from "zustand/shallow" import type { EditorView } from "@codemirror/view" -import { DateInput } from "@mantine/dates" import { useQueryClient } from "@tanstack/react-query" import { DashboardMain } from "~/components/dashboard/DashboardMain" @@ -16,14 +14,14 @@ import { EditorToolbar } from "~/components/dashboard/EditorToolbar" import { OptionsButton } from "~/components/dashboard/OptionsButton" import { PublishButton } from "~/components/dashboard/PublishButton" import PublishedModal from "~/components/dashboard/PublishedModal" +import EditorCover from "~/components/dashboard/editor-properties/EditorCover" +import EditorDisableAISummary from "~/components/dashboard/editor-properties/EditorDisableAISummary" +import EditorExcerpt from "~/components/dashboard/editor-properties/EditorExcerpt" +import EditorPublishAt from "~/components/dashboard/editor-properties/EditorPublishAt" +import EditorSlug from "~/components/dashboard/editor-properties/EditorSlug" +import EditorTags from "~/components/dashboard/editor-properties/EditorTags" import { Button } from "~/components/ui/Button" -import { FieldLabel } from "~/components/ui/FieldLabel" -import { ImageUploader } from "~/components/ui/ImageUploader" -import { Input } from "~/components/ui/Input" import { useModalStack } from "~/components/ui/ModalStack" -import { Switch } from "~/components/ui/Switch" -import { TagInput } from "~/components/ui/TagInput" -import { UniLink } from "~/components/ui/UniLink" import { Values, initialEditorState, @@ -41,12 +39,11 @@ import { useTranslation } from "~/lib/i18n/client" import { getPageVisibility } from "~/lib/page-helpers" import { delStorage, setStorage } from "~/lib/storage" import { ExpandedNote, NoteType, PageVisibilityEnum } from "~/lib/types" -import { cn, pick } from "~/lib/utils" +import { cn } from "~/lib/utils" import { checkPageSlug } from "~/models/page.model" import { useCreatePage, useDeletePage, - useGetDistinctNoteTagsOfCharacter, useGetPage, useUpdatePage, } from "~/queries/page" @@ -111,8 +108,6 @@ export default function PostEditor() { handle: subdomain, }) - const userTags = useGetDistinctNoteTagsOfCharacter(site.data?.characterId) - const [visibility, setVisibility] = useState() useEffect(() => { @@ -138,31 +133,31 @@ export default function PostEditor() { const getDraftKey = useGetState(draftKey) const updateValue = useCallback( - (key: K, value: Values[K]) => { + (val: Partial) => { if (visibility !== PageVisibilityEnum.Draft) { setVisibility(PageVisibilityEnum.Modified) } const values = getValues() const draftKey = getDraftKey() - if (key === "title") { + if (val.title) { setDefaultSlug( getDefaultSlug( - value as string, + val.title, draftKey.replace(`draft-${site.data?.characterId}-`, ""), ), ) } - if (key === "slug" && !/^[a-zA-Z0-9\-_]*$/.test(value as string)) { + if (val.slug && !/^[a-zA-Z0-9\-_]*$/.test(val.slug)) { // Replace all invalid chars - ;(value as string) = (value as string).replace(/[^\w\-]/g, "-") + val.slug = val.slug.replace(/[^\w\-]/g, "-") toast.error( t( "Slug can only contain letters, numbers, hyphens, and underscores.", ), ) } - const newValues = { ...values, [key]: value } + const newValues = { ...values, ...val } if (draftKey) { setStorage(draftKey, { date: +new Date(), @@ -365,7 +360,9 @@ export default function PostEditor() { const onChange = useCallback( (value: string) => { - updateValue("content", value) + updateValue({ + content: value, + }) }, [updateValue], ) @@ -384,8 +381,11 @@ export default function PostEditor() { defaultSlug={defaultSlug} updateValue={updateValue} type={type} - subdomain={subdomain} - userTags={userTags.data?.list || []} + characterId={site.data?.characterId} + siteLink={getSiteLink({ + subdomain, + domain: site.data?.metadata?.content?.custom_domain, + })} /> ) @@ -499,7 +499,11 @@ export default function PostEditor() { view?.focus() } }} - onChange={(e) => updateValue("title", e.target.value)} + onChange={(e) => + updateValue({ + title: e.target.value, + }) + } className="h-12 ml-1 inline-flex items-center border-none text-3xl font-bold w-full focus:outline-none bg-white" placeholder={t("Title goes here...") || ""} /> @@ -527,30 +531,32 @@ const EditorExtraProperties = memo( ({ type, updateValue, - subdomain, defaultSlug, - userTags, + characterId, + siteLink, }: { - updateValue: (key: K, value: Values[K]) => void + updateValue: (val: Partial) => void type: NoteType - subdomain: string defaultSlug: string - userTags: string[] + characterId?: number + siteLink?: string }) => { - const values = useEditorState( - (state) => - pick(state, ["publishedAt", "slug", "excerpt", "tags", "cover"]), - shallow, - ) const { t } = useTranslation("dashboard") - const site = useGetSite(subdomain) const { present } = useModalStack() const openAdvancedOptions = () => { present({ title: t("Advanced Settings"), content: () => ( - +
+ + +
), modalProps: { withConfirm: true, @@ -560,99 +566,21 @@ const EditorExtraProperties = memo( return (
-
- - { - const { address, mime_type } = key as { - address?: string - mime_type?: string - } - updateValue("cover", { - address, - mime_type, - }) - }} - accept="image/*" - /> -
- {t("Leave blank to use the first image in the post")} -
-
-
- ( - updateValue("tags", value)} - /> - )} - /> -
-
- ) => - updateValue("slug", e.target.value) - } - help={ - <> - {(values.slug || defaultSlug) && ( - <> - {t(`This ${type} will be accessible at`)}{" "} - - {getSiteLink({ - subdomain, - domain: site.data?.metadata?.content?.custom_domain, - noProtocol: true, - })} - /{encodeURIComponent(values.slug || defaultSlug)} - - - )} - - } - /> -
-
- ) => { - updateValue("excerpt", e.target.value) - }} - help={t("Leave it blank to use auto-generated excerpt")} - /> -
+ + + +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorCover.tsx b/src/components/dashboard/editor-properties/EditorCover.tsx new file mode 100644 index 00000000..253ff15b --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorCover.tsx @@ -0,0 +1,39 @@ +import { FieldLabel } from "~/components/ui/FieldLabel" +import { ImageUploader } from "~/components/ui/ImageUploader" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorCover({ + updateValue, + prompt, +}: { + updateValue: (val: Partial) => void + prompt?: string +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.cover) + + return ( +
+ + { + const { address, mime_type } = key as Values["cover"] + updateValue({ + cover: { + address, + mime_type, + }, + }) + }} + accept="image/*" + /> + {prompt &&
{prompt}
} +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorDisableAISummary.tsx b/src/components/dashboard/editor-properties/EditorDisableAISummary.tsx new file mode 100644 index 00000000..df1b7156 --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorDisableAISummary.tsx @@ -0,0 +1,27 @@ +import { Switch } from "~/components/ui/Switch" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorDisableAISummary({ + updateValue, +}: { + updateValue: (val: Partial) => void +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.disableAISummary) + + return ( +
+ + + updateValue({ + disableAISummary: state, + }) + } + /> +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorExcerpt.tsx b/src/components/dashboard/editor-properties/EditorExcerpt.tsx new file mode 100644 index 00000000..604130b3 --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorExcerpt.tsx @@ -0,0 +1,36 @@ +import { ChangeEvent } from "react" + +import { Input } from "~/components/ui/Input" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorExcerpt({ + updateValue, + prompt, +}: { + updateValue: (val: Partial) => void + prompt?: string +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.excerpt) + + return ( +
+ ) => { + updateValue({ + excerpt: e.target.value, + }) + }} + help={prompt} + /> +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorExternalUrl.tsx b/src/components/dashboard/editor-properties/EditorExternalUrl.tsx new file mode 100644 index 00000000..fd92c0c6 --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorExternalUrl.tsx @@ -0,0 +1,31 @@ +import { ChangeEvent } from "react" + +import { Input } from "~/components/ui/Input" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorExternalUrl({ + updateValue, +}: { + updateValue: (val: Partial) => void +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.externalUrl) + + return ( +
+ ) => { + updateValue({ + externalUrl: e.target.value, + }) + }} + /> +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorPublishAt.tsx b/src/components/dashboard/editor-properties/EditorPublishAt.tsx new file mode 100644 index 00000000..1b93422f --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorPublishAt.tsx @@ -0,0 +1,61 @@ +import { DateInput } from "@mantine/dates" + +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorPublishAt({ + updateValue, + prompt, +}: { + updateValue: (val: Partial) => void + prompt?: string +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.publishedAt) + + return ( +
+ + { + if (value) { + updateValue({ + publishedAt: value.toISOString(), + }) + } else { + updateValue({ + publishedAt: "", + }) + } + }} + styles={{ + input: { + borderRadius: "0.5rem", + borderColor: "var(--border-color)", + height: "2.5rem", + "&:focus-within": { + borderColor: "var(--theme-color)", + }, + }, + }} + /> + {prompt &&
{prompt}
} + {value && value > new Date().toISOString() && ( +
+ {t( + "The post is currently not public as its publication date has been scheduled for a future time.", + )} +
+ )} +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorSlug.tsx b/src/components/dashboard/editor-properties/EditorSlug.tsx new file mode 100644 index 00000000..7988976a --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorSlug.tsx @@ -0,0 +1,56 @@ +import { ChangeEvent } from "react" + +import { Input } from "~/components/ui/Input" +import { UniLink } from "~/components/ui/UniLink" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorSlug({ + updateValue, + defaultValue, + type, + siteLink, +}: { + updateValue: (val: Partial) => void + defaultValue?: string + type: string + siteLink?: string +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.slug) + + return ( +
+ ) => + updateValue({ + slug: e.target.value, + }) + } + help={ + <> + {(value || defaultValue) && ( + <> + {t(`This ${type} will be accessible at`)}{" "} + + {siteLink}/{encodeURIComponent(value || defaultValue || "")} + + + )} + + } + /> +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorTags.tsx b/src/components/dashboard/editor-properties/EditorTags.tsx new file mode 100644 index 00000000..95011a91 --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorTags.tsx @@ -0,0 +1,41 @@ +import { Input } from "~/components/ui/Input" +import { TagInput } from "~/components/ui/TagInput" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" +import { useGetDistinctNoteTagsOfCharacter } from "~/queries/page" + +export default function EditorTags({ + updateValue, + characterId, +}: { + updateValue: (val: Partial) => void + characterId?: number +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.tags) + + const userTags = useGetDistinctNoteTagsOfCharacter(characterId) + + return ( +
+ ( + + updateValue({ + tags: value, + }) + } + /> + )} + /> +
+ ) +} diff --git a/src/components/dashboard/editor-properties/EditorTitle.tsx b/src/components/dashboard/editor-properties/EditorTitle.tsx new file mode 100644 index 00000000..4f421123 --- /dev/null +++ b/src/components/dashboard/editor-properties/EditorTitle.tsx @@ -0,0 +1,31 @@ +import { ChangeEvent } from "react" + +import { Input } from "~/components/ui/Input" +import { Values, useEditorState } from "~/hooks/useEditorState" +import { useTranslation } from "~/lib/i18n/client" + +export default function EditorTitle({ + updateValue, +}: { + updateValue: (val: Partial) => void +}) { + const { t } = useTranslation("dashboard") + const value = useEditorState((state) => state.title) + + return ( +
+ ) => { + updateValue({ + title: e.target.value, + }) + }} + /> +
+ ) +} diff --git a/src/lib/i18n/locales/zh/dashboard.json b/src/lib/i18n/locales/zh/dashboard.json index b7c5e940..4afa7828 100644 --- a/src/lib/i18n/locales/zh/dashboard.json +++ b/src/lib/i18n/locales/zh/dashboard.json @@ -76,8 +76,7 @@ "This page will be accessible from this time. Leave blank to use the current time.": "此页面将从此时间开始可访问。留空则使用当前时间。", "This portfolio will be accessible from this time. Leave blank to use the current time.": "此作品将从此时间开始可访问。留空则使用当前时间。", "The post is currently not public as its publication date has been scheduled for a future time.": "此文章当前没有公开,因为它的发布日期已被定为将来的某个时间。", - "Post slug": "文章短链接", - "Page slug": "页面短链接", + "Slug": "短链接", "This post will be accessible at": "此文章将可通过以下链接访问", "This page will be accessible at": "此页面将可通过以下链接访问", "Tags": "标签",