fix(news): restore publication flow and deduplicate dashboard friends
This commit is contained in:
1 parent
ef94646d60
commit
9b0ea2fb16
22 files changed
+638
-195
No files matched your search
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user