fix(news): restore publication flow and deduplicate dashboard friends
CI / check (push) Successful in 1m6s
CI / deploy (push) Successful in 59s

This commit is contained in:
Simo committed 2026-09-06 18:32:43 +02:00
1 parent ef94646d60
commit 9b0ea2fb16
22 files changed
+638 -195

No files matched your search

+38 -11
View File
@@ -1,8 +1,10 @@
"use client";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useMemo, useRef, useState, useTransition } from "react";
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";
@@ -12,7 +14,7 @@ export function ArticleForm({
action,
defaultValues,
}: {
action: (formData: FormData) => Promise<void>;
action: (formData: FormData) => Promise<ArticleSaveResult | void>;
edit?: boolean;
defaultValues?: {
title?: string;
@@ -28,7 +30,8 @@ export function ArticleForm({
const tAction = useTranslations("pages.admin.actions");
const [dirty, setDirty] = useState(false);
const [pending, startTransition] = useTransition();
const [saveError, setSaveError] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const router = useRouter();
const saving = useRef(false);
const editVersion = useRef(0);
function markDirty() {
@@ -42,6 +45,14 @@ export function ArticleForm({
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<HTMLFormElement>(null);
const [preview, setPreview] = useState<ArticlePreviewData | null>(null);
@@ -55,13 +66,24 @@ export function ArticleForm({
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(false);
setSaveError(null);
startTransition(async () => {
try {
await action(data);
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 &&
@@ -72,7 +94,7 @@ export function ArticleForm({
setDirty(false);
throw error;
}
setSaveError(true);
setSaveError(tAction("saveError"));
} finally {
saving.current = false;
}
@@ -84,7 +106,7 @@ export function ArticleForm({
<fieldset disabled={pending} className="contents">
{saveError && (
<p role="alert" className="text-sm">
{tAction("saveError")}
{saveError}
</p>
)}
<div className="grid gap-4 md:grid-cols-2">
@@ -94,6 +116,7 @@ export function ArticleForm({
</span>
<input
name="title"
maxLength={255}
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t("titlePlaceholder")}
@@ -108,6 +131,7 @@ export function ArticleForm({
</span>
<input
name="slug"
maxLength={255}
value={slugTouched ? slug : suggestedSlug}
onChange={(e) => {
setSlug(e.target.value);
@@ -127,6 +151,7 @@ export function ArticleForm({
<input
aria-label={t("image")}
name="image"
maxLength={255}
value={image}
onChange={(e) => {
setImage(e.target.value);
@@ -148,7 +173,7 @@ export function ArticleForm({
// eslint-disable-next-line @next/next/no-img-element
<img
src={image}
alt="preview"
alt={t("imagePreview")}
onError={() => setImageError(true)}
className="mt-2 rounded-lg border border-[var(--admin-text-muted)]/14 object-cover"
style={{ maxHeight: 160 }}
@@ -156,17 +181,18 @@ export function ArticleForm({
) : null}
{imageError ? (
<p className="muted mt-1" style={{ fontSize: "0.78rem" }}>
Image not found — enter a valid URL or browse.
{t("imageNotFound")}
</p>
) : null}
</div>
<label className="block">
<span className="text-xs font-medium text-[var(--admin-text-muted)]">
{t("body")}
{t("summary")}
</span>
<textarea
name="shortStory"
maxLength={255}
defaultValue={defaultValues?.shortStory ?? ""}
placeholder={t("bodyPlaceholder")}
rows={2}
@@ -212,7 +238,8 @@ export function ArticleForm({
<input
type="datetime-local"
name="publishAt"
defaultValue={defaultValues?.publishAt ?? ""}
value={publishAt}
onChange={(e) => setPublishAt(e.target.value)}
required
className="input input-bordered mt-1 w-full"
/>