"use client"; 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 { ArticleSaveResult } from "@/lib/article-input"; import { slugify } from "@/lib/format"; import { ArticlePreview, type ArticlePreviewData } from "./article-preview"; import { MediaPicker } from "./media-picker"; import { RichText } from "./rich-text"; export function ArticleForm({ action, defaultValues, }: { action: (formData: FormData) => Promise; edit?: boolean; defaultValues?: { title?: string; slug?: string; image?: string; shortStory?: string; fullStory?: string; status?: string; publishAt?: string; }; }) { const t = useTranslations("pages.admin.articles.form"); const tAction = useTranslations("pages.admin.actions"); const [dirty, setDirty] = useState(false); const [pending, startTransition] = useTransition(); const [saveError, setSaveError] = useState(null); const router = useRouter(); const saving = useRef(false); const editVersion = useRef(0); function markDirty() { editVersion.current++; setDirty(true); } useUnsavedChanges(dirty, tAction("leaveUnsaved")); const [title, setTitle] = useState(defaultValues?.title ?? ""); const [slug, setSlug] = useState(defaultValues?.slug ?? ""); const [slugTouched, setSlugTouched] = useState(Boolean(defaultValues?.slug)); const [image, setImage] = useState(defaultValues?.image ?? ""); const [imageError, setImageError] = useState(false); const [status, setStatus] = useState(defaultValues?.status ?? "published"); const [publishAt, setPublishAt] = useState(""); useEffect(() => { if (!defaultValues?.publishAt) return; const date = new Date(defaultValues.publishAt); if (Number.isNaN(date.getTime())) return; const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); setPublishAt(local.toISOString().slice(0, 16)); }, [defaultValues?.publishAt]); const formRef = useRef(null); const [preview, setPreview] = useState(null); const suggestedSlug = useMemo(() => slugify(title), [title]); return (
{ event.preventDefault(); if (saving.current) return; const data = new FormData(event.currentTarget); if (status === "scheduled" && publishAt) { const date = new Date(publishAt); if (!Number.isNaN(date.getTime())) data.set("publishAt", date.toISOString()); } const submittedVersion = editVersion.current; saving.current = true; setSaveError(null); startTransition(async () => { try { const result = await action(data); if (result && !result.ok) { setSaveError(result.error); return; } if (editVersion.current === submittedVersion) setDirty(false); if (result?.ok && result.data?.redirectTo) router.push(result.data.redirectTo); } catch (error) { if ( error && typeof error === "object" && "digest" in error && String(error.digest).startsWith("NEXT_REDIRECT") ) { setDirty(false); throw error; } setSaveError(tAction("saveError")); } finally { saving.current = false; } }); }} className="admin-card" style={{ display: "grid", gap: "1rem" }} >
{saveError && (

{saveError}

)}
{t("image")}
{ setImage(e.target.value); setImageError(false); }} placeholder={t("imagePlaceholder")} className="input input-bordered min-w-0 flex-1" /> { setImage(url); markDirty(); setImageError(false); }} current={image} />
{image && !imageError ? ( // eslint-disable-next-line @next/next/no-img-element {t("imagePreview")} setImageError(true)} className="mt-2 rounded-lg border border-[var(--admin-text-muted)]/14 object-cover" style={{ maxHeight: 160 }} /> ) : null} {imageError ? (

{t("imageNotFound")}

) : null}