feat: use unified editor properties

This commit is contained in:
DIYgod 2023-08-25 03:03:05 +01:00
parent b3bf66f972
commit 697ee50cf1
No known key found for this signature in database
12 changed files with 482 additions and 359 deletions

View File

@ -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<Values>) => {
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() {
</div>
) : (
<>
<EditorExtraProperties />
<EditorExtraProperties updateValue={updateValue} />
<div className="flex justify-between h-14 items-center text-sm mt-8">
<div className="flex items-center space-x-3 flex-shrink-0">
<PublishButton
@ -281,155 +312,28 @@ export default function PortfolioEditor() {
)
}
const EditorExtraProperties = memo(() => {
const values = useEditorState()
const { t } = useTranslation("dashboard")
const updateValue = useEditorState.setState
const EditorExtraProperties = memo(
({ updateValue }: { updateValue: (val: Partial<Values>) => 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 (
<div className="w-full space-y-5">
<div>
<Input
label={t("External Url") || ""}
isBlock
name="externalUrl"
id="externalUrl"
value={values.externalUrl}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue({
externalUrl: e.target.value,
})
}}
/>
</div>
<Button onClick={autofill} isLoading={filling}>
{t("Autofill")}
</Button>
<div>
<FieldLabel label={t("Cover Image")} />
<ImageUploader
id="icon"
className="aspect-video rounded-lg w-[480px]"
value={values.cover as any}
hasClose={true}
withMimeType={true}
uploadEnd={(key) => {
const { address, mime_type } = key as {
address?: string
mime_type?: string
}
updateValue({
cover: {
address,
mime_type,
},
})
}}
accept="image/*"
/>
</div>
<div>
<Input
label={t("Title") || ""}
isBlock
name="title"
id="title"
value={values.title}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue({
title: e.target.value,
})
}}
/>
</div>
<div>
<Input
label={t("Excerpt") || ""}
isBlock
name="excerpt"
id="excerpt"
value={values.excerpt}
multiline
rows={4}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
updateValue({
excerpt: e.target.value,
})
}}
/>
</div>
<div>
<label className="form-label" htmlFor="publishAt">
{t("Publish at")}
</label>
<DateInput
className="[&_input]:text-black/90 [&_input]:bg-white"
allowDeselect
clearable
valueFormat="YYYY-MM-DD, h:mm a"
name="publishAt"
id="publishAt"
value={values.publishedAt ? new Date(values.publishedAt) : undefined}
onChange={(value: Date | null) => {
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)",
},
},
}}
/>
<div className="text-xs text-gray-400 mt-1">
{t(
`This portfolio will be accessible from this time. Leave blank to use the current time.`,
)}
return (
<div className="w-full space-y-5">
<EditorExternalUrl updateValue={updateValue} />
<EditorAutofill updateValue={updateValue} />
<div className="w-[480px]">
<EditorCover updateValue={updateValue} />
</div>
{values.publishedAt > new Date().toISOString() && (
<div className="text-xs mt-1 text-orange-500">
{t(
"The post is currently not public as its publication date has been scheduled for a future time.",
)}
</div>
)}
<EditorTitle updateValue={updateValue} />
<EditorExcerpt updateValue={updateValue} />
<EditorPublishAt
updateValue={updateValue}
prompt={t(
"This portfolio will be accessible from this time. Leave blank to use the current time.",
)}
/>
</div>
</div>
)
})
)
},
)
EditorExtraProperties.displayName = "EditorExtraProperties"

View File

@ -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<PageVisibilityEnum>()
useEffect(() => {
@ -138,31 +133,31 @@ export default function PostEditor() {
const getDraftKey = useGetState(draftKey)
const updateValue = useCallback(
<K extends keyof Values>(key: K, value: Values[K]) => {
(val: Partial<Values>) => {
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: <K extends keyof Values>(key: K, value: Values[K]) => void
updateValue: (val: Partial<Values>) => 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: () => (
<EditorAdvancedModal type={type} updateValue={updateValue} />
<div className="p-5 space-y-5">
<EditorDisableAISummary updateValue={updateValue} />
<EditorPublishAt
updateValue={updateValue}
prompt={t(
`This ${type} will be accessible from this time. Leave blank to use the current time.`,
)}
/>
</div>
),
modalProps: {
withConfirm: true,
@ -560,99 +566,21 @@ const EditorExtraProperties = memo(
return (
<div className="h-full overflow-auto w-[280px] border-l bg-zinc-50 p-5 space-y-5">
<div>
<FieldLabel label={t("Cover Image")} />
<ImageUploader
id="icon"
className="aspect-video rounded-lg"
value={values.cover as any}
hasClose={true}
withMimeType={true}
uploadEnd={(key) => {
const { address, mime_type } = key as {
address?: string
mime_type?: string
}
updateValue("cover", {
address,
mime_type,
})
}}
accept="image/*"
/>
<div className="text-xs text-gray-400 mt-1">
{t("Leave blank to use the first image in the post")}
</div>
</div>
<div>
<Input
name="tags"
value={values.tags}
label={t("Tags") || ""}
id="tags"
isBlock
renderInput={(props) => (
<TagInput
{...props}
userTags={userTags}
onTagChange={(value: string) => updateValue("tags", value)}
/>
)}
/>
</div>
<div>
<Input
name="slug"
value={values.slug}
placeholder={defaultSlug}
label={
t(`${type.charAt(0).toUpperCase() + type.slice(1)} slug`) || ""
}
id="slug"
isBlock
onChange={(e: ChangeEvent<HTMLInputElement>) =>
updateValue("slug", e.target.value)
}
help={
<>
{(values.slug || defaultSlug) && (
<>
{t(`This ${type} will be accessible at`)}{" "}
<UniLink
href={`${getSiteLink({
subdomain,
domain: site.data?.metadata?.content?.custom_domain,
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
className="hover:underline"
>
{getSiteLink({
subdomain,
domain: site.data?.metadata?.content?.custom_domain,
noProtocol: true,
})}
/{encodeURIComponent(values.slug || defaultSlug)}
</UniLink>
</>
)}
</>
}
/>
</div>
<div>
<Input
label={t("Excerpt") || ""}
isBlock
name="excerpt"
id="excerpt"
value={values.excerpt}
multiline
rows={4}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
updateValue("excerpt", e.target.value)
}}
help={t("Leave it blank to use auto-generated excerpt")}
/>
</div>
<EditorCover
updateValue={updateValue}
prompt={t("Leave blank to use the first image in the post")}
/>
<EditorTags updateValue={updateValue} characterId={characterId} />
<EditorSlug
updateValue={updateValue}
defaultValue={defaultSlug}
type={type}
siteLink={siteLink}
/>
<EditorExcerpt
updateValue={updateValue}
prompt={t("Leave it blank to use auto-generated excerpt")}
/>
<div>
<Button
variant="secondary"
@ -673,76 +601,3 @@ const EditorExtraProperties = memo(
)
EditorExtraProperties.displayName = "EditorExtraProperties"
const EditorAdvancedModal = ({
type,
updateValue,
}: {
type: NoteType
updateValue: <K extends keyof Values>(key: K, value: Values[K]) => void
}) => {
const { t } = useTranslation("dashboard")
const values = useEditorState(
(state) => pick(state, ["disableAISummary", "publishedAt"]),
shallow,
)
return (
<div className="p-5 space-y-5">
<div>
<label className="form-label">
{t("Disable AI-generated summary")}
</label>
<Switch
label=""
checked={values.disableAISummary}
setChecked={(state) => updateValue("disableAISummary", state)}
/>
</div>
<div>
<label className="form-label" htmlFor="publishAt">
{t("Publish at")}
</label>
<DateInput
className="[&_input]:text-black/90 [&_input]:bg-white"
allowDeselect
clearable
valueFormat="YYYY-MM-DD, h:mm a"
name="publishAt"
id="publishAt"
value={values.publishedAt ? new Date(values.publishedAt) : undefined}
onChange={(value: Date | null) => {
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)",
},
},
}}
/>
<div className="text-xs text-gray-400 mt-1">
{t(
`This ${type} will be accessible from this time. Leave blank to use the current time.`,
)}
</div>
{values.publishedAt > new Date().toISOString() && (
<div className="text-xs mt-1 text-orange-500">
{t(
"The post is currently not public as its publication date has been scheduled for a future time.",
)}
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,43 @@
import { useState } from "react"
import { Button } from "~/components/ui/Button"
import { Values, useEditorState } from "~/hooks/useEditorState"
import { useTranslation } from "~/lib/i18n/client"
export default function EditorAutofill({
updateValue,
}: {
updateValue: (val: Partial<Values>) => void
}) {
const { t } = useTranslation("dashboard")
const externalUrl = useEditorState((state) => state.externalUrl)
const [filling, setFilling] = useState(false)
const autofill = async () => {
setFilling(true)
const result = await (
await fetch(`/api/open-graph?url=${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 (
<Button onClick={autofill} isLoading={filling}>
{t("Autofill")}
</Button>
)
}

View File

@ -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<Values>) => void
prompt?: string
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.cover)
return (
<div>
<FieldLabel label={t("Cover Image")} />
<ImageUploader
id="icon"
className="aspect-video rounded-lg"
value={value as any}
hasClose={true}
withMimeType={true}
uploadEnd={(key) => {
const { address, mime_type } = key as Values["cover"]
updateValue({
cover: {
address,
mime_type,
},
})
}}
accept="image/*"
/>
{prompt && <div className="text-xs text-gray-400 mt-1">{prompt}</div>}
</div>
)
}

View File

@ -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<Values>) => void
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.disableAISummary)
return (
<div>
<label className="form-label">{t("Disable AI-generated summary")}</label>
<Switch
label=""
checked={value}
setChecked={(state) =>
updateValue({
disableAISummary: state,
})
}
/>
</div>
)
}

View File

@ -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<Values>) => void
prompt?: string
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.excerpt)
return (
<div>
<Input
label={t("Excerpt") || ""}
isBlock
name="excerpt"
id="excerpt"
value={value}
multiline
rows={4}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
updateValue({
excerpt: e.target.value,
})
}}
help={prompt}
/>
</div>
)
}

View File

@ -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<Values>) => void
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.externalUrl)
return (
<div>
<Input
label={t("External Url") || ""}
isBlock
name="externalUrl"
id="externalUrl"
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue({
externalUrl: e.target.value,
})
}}
/>
</div>
)
}

View File

@ -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<Values>) => void
prompt?: string
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.publishedAt)
return (
<div>
<label className="form-label" htmlFor="publishAt">
{t("Publish at")}
</label>
<DateInput
className="[&_input]:text-black/90 [&_input]:bg-white"
allowDeselect
clearable
valueFormat="YYYY-MM-DD, h:mm a"
name="publishAt"
id="publishAt"
value={value ? new Date(value) : undefined}
onChange={(value: Date | null) => {
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 && <div className="text-xs text-gray-400 mt-1">{prompt}</div>}
{value && value > new Date().toISOString() && (
<div className="text-xs mt-1 text-orange-500">
{t(
"The post is currently not public as its publication date has been scheduled for a future time.",
)}
</div>
)}
</div>
)
}

View File

@ -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<Values>) => void
defaultValue?: string
type: string
siteLink?: string
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.slug)
return (
<div>
<Input
name="slug"
value={value}
placeholder={defaultValue}
label={t("Slug") || ""}
id="slug"
isBlock
onChange={(e: ChangeEvent<HTMLInputElement>) =>
updateValue({
slug: e.target.value,
})
}
help={
<>
{(value || defaultValue) && (
<>
{t(`This ${type} will be accessible at`)}{" "}
<UniLink
href={`${siteLink}/${encodeURIComponent(
value || defaultValue || "",
)}`}
className="hover:underline"
>
{siteLink}/{encodeURIComponent(value || defaultValue || "")}
</UniLink>
</>
)}
</>
}
/>
</div>
)
}

View File

@ -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<Values>) => void
characterId?: number
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.tags)
const userTags = useGetDistinctNoteTagsOfCharacter(characterId)
return (
<div>
<Input
name="tags"
value={value}
label={t("Tags") || ""}
id="tags"
isBlock
renderInput={(props) => (
<TagInput
{...props}
userTags={userTags.data?.list ?? []}
onTagChange={(value: string) =>
updateValue({
tags: value,
})
}
/>
)}
/>
</div>
)
}

View File

@ -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<Values>) => void
}) {
const { t } = useTranslation("dashboard")
const value = useEditorState((state) => state.title)
return (
<div>
<Input
label={t("Title") || ""}
isBlock
name="title"
id="title"
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue({
title: e.target.value,
})
}}
/>
</div>
)
}

View File

@ -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": "标签",