feat(cms): improve catalog, editorial recovery and operations
CI / check (push) Successful in 52s
CI / deploy (push) Successful in 2m10s
CI / publish-container (push) Failing after 1m18s

This commit is contained in:
Simo committed 2026-09-09 19:36:15 +02:00
1 parent 2fead134e6
commit c389c3893d
122 files changed
+8137 -703

No files matched your search

+39 -2
View File
@@ -4,18 +4,24 @@ import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useMemo, useRef, useState, useTransition } from "react";
import { useUnsavedChanges } from "@/hooks/use-unsaved-changes";
import type { ArticleDraft } from "@/lib/article-draft";
import type { ArticleSaveResult } from "@/lib/article-input";
import { slugify } from "@/lib/format";
import { ArticlePreview, type ArticlePreviewData } from "./article-preview";
import { useArticleRecovery } from "./article-recovery";
import { MediaPicker } from "./media-picker";
import { RichText } from "./rich-text";
export function ArticleForm({
action,
defaultValues,
articleKey = "new",
baseToken = "",
}: {
action: (formData: FormData) => Promise<ArticleSaveResult | undefined>;
edit?: boolean;
articleKey?: string;
baseToken?: string;
defaultValues?: {
title?: string;
slug?: string;
@@ -55,6 +61,31 @@ export function ArticleForm({
}, [defaultValues?.publishAt]);
const formRef = useRef<HTMLFormElement>(null);
const [token, setToken] = useState(baseToken);
const [summary, setSummary] = useState(defaultValues?.shortStory ?? "");
const [body, setBody] = useState(defaultValues?.fullStory ?? "");
const [bodyVersion, setBodyVersion] = useState(0);
function restoreDraft(draft: ArticleDraft, revision: boolean) {
setTitle(draft.title);
setSlug(draft.slug);
setSlugTouched(true);
setImage(draft.image);
setImageError(false);
setSummary(draft.shortStory);
setBody(draft.fullStory);
setBodyVersion((v) => v + 1);
setStatus("draft");
setPublishAt("");
setToken(revision ? baseToken : draft.baseToken);
markDirty();
}
const recovery = useArticleRecovery(
articleKey,
formRef,
saving,
dirty,
restoreDraft,
);
const [preview, setPreview] = useState<ArticlePreviewData | null>(null);
const suggestedSlug = useMemo(() => slugify(title), [title]);
@@ -76,12 +107,14 @@ export function ArticleForm({
setSaveError(null);
startTransition(async () => {
try {
await recovery.wait();
const result = await action(data);
if (result && !result.ok) {
setSaveError(result.error);
return;
}
if (editVersion.current === submittedVersion) setDirty(false);
if (result?.ok) await recovery.clear().catch(() => {});
if (result?.ok && result.data?.redirectTo)
router.push(result.data.redirectTo);
} catch (error) {
@@ -104,6 +137,8 @@ export function ArticleForm({
style={{ display: "grid", gap: "1rem" }}
>
<fieldset disabled={pending} className="contents">
<input type="hidden" name="baseToken" value={token} />
{recovery.panel}
{saveError && (
<p role="alert" className="text-sm">
{saveError}
@@ -193,7 +228,8 @@ export function ArticleForm({
<textarea
name="shortStory"
maxLength={255}
defaultValue={defaultValues?.shortStory ?? ""}
value={summary}
onChange={(event) => setSummary(event.target.value)}
placeholder={t("bodyPlaceholder")}
rows={2}
className="input input-bordered mt-1 w-full resize-y"
@@ -205,9 +241,10 @@ export function ArticleForm({
{t("body")}
</span>
<RichText
key={bodyVersion}
onChange={markDirty}
name="fullStory"
defaultValue={defaultValues?.fullStory ?? ""}
defaultValue={body}
placeholder={t("bodyPlaceholder")}
rows={12}
/>