diff --git a/src/actions/admin-articles.ts b/src/actions/admin-articles.ts index e6ccc0ea..939408ee 100644 --- a/src/actions/admin-articles.ts +++ b/src/actions/admin-articles.ts @@ -36,13 +36,14 @@ export async function createArticle(formData: FormData): Promise { const image = String(formData.get("image") ?? "") .normalize("NFC") .trim(); + const rawSlug = String(formData.get("slug") ?? "").trim(); if (!title) return; try { const now = new Date(); await prisma.websiteArticles.create({ data: { - slug: await uniqueSlug(title), + slug: rawSlug ? await uniqueSlug(rawSlug) : await uniqueSlug(title), title: title.slice(0, 255), shortStory: shortStory.slice(0, 255), fullStory, @@ -64,6 +65,7 @@ export async function createArticle(formData: FormData): Promise { export async function updateArticle(formData: FormData): Promise { await requirePermission(PERMS.NEWS_EDIT); const id = BigInt(String(formData.get("id"))); + const rawSlug = String(formData.get("slug") ?? "").trim(); try { await prisma.websiteArticles.update({ where: { id }, @@ -72,6 +74,9 @@ export async function updateArticle(formData: FormData): Promise { .normalize("NFC") .trim() .slice(0, 255), + ...(rawSlug + ? { slug: await uniqueSlug(rawSlug) } + : {}), shortStory: String(formData.get("shortStory") ?? "") .normalize("NFC") .trim() diff --git a/src/app/admin/articles/[id]/page.tsx b/src/app/admin/articles/[id]/page.tsx index 66ab9b99..74ce528b 100644 --- a/src/app/admin/articles/[id]/page.tsx +++ b/src/app/admin/articles/[id]/page.tsx @@ -67,6 +67,7 @@ export default async function EditArticle({ }} defaultValues={{ title: article.title, + slug: article.slug, image: article.image, shortStory: article.shortStory, fullStory: article.fullStory, diff --git a/src/app/admin/articles/page.tsx b/src/app/admin/articles/page.tsx index 9845ccbd..97bc5eb0 100644 --- a/src/app/admin/articles/page.tsx +++ b/src/app/admin/articles/page.tsx @@ -23,18 +23,45 @@ export default async function AdminArticles({ const articles = await prisma.websiteArticles .findMany({ orderBy: { createdAt: "desc" }, - select: { id: true, title: true, slug: true, createdAt: true }, + select: { + id: true, + title: true, + slug: true, + image: true, + createdAt: true, + userId: true, + }, take: 50, }) .catch(() => []); + const authorMap = new Map(); + const authorIds = articles.map((a) => a.userId).filter((id): id is number => Boolean(id)); + if (authorIds.length > 0) { + const authors = await prisma.user + .findMany({ + where: { id: { in: authorIds } }, + select: { id: true, username: true }, + }) + .catch(() => []); + for (const u of authors) authorMap.set(u.id, u.username); + } + const latest = articles[0]?.createdAt ? articles[0].createdAt.toISOString().slice(0, 10) : "—"; return (
-
+
+
+

+ {t("title")} +

+

+ {t("subtitle")} +

+
+ {t("newArticle")} @@ -65,47 +92,71 @@ export default async function AdminArticles({ {articles.length === 0 ? (
{t("noArticles")}
) : ( - - - - - - - - - - - {articles.map((a) => ( - - - - - - - ))} - -
{t("colTitle")}{t("colSlug")}{t("colCreated")}{t("colActions")}
- {a.title} - - {a.slug} - - {a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "—"} - -
- + {articles.map((a) => ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {a.title}, + ) => { + e.currentTarget.style.visibility = "hidden"; + }} + /> +
+
+ + {a.title} + +

+ {a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "—"} + {authorMap.get(a.userId ?? -1) ? ( + <> + · + {authorMap.get(a.userId ?? -1)} + + ) : null} +

+
+ + {t("edit")} + +
+ + -
-
-
+ {t("delete")} + + +
+
+ + ))} + )}
diff --git a/src/components/admin/article-form.tsx b/src/components/admin/article-form.tsx index 01f9a0d4..6ba21e0a 100644 --- a/src/components/admin/article-form.tsx +++ b/src/components/admin/article-form.tsx @@ -1,40 +1,73 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; import { MediaPicker } from "./media-picker"; import { RichText } from "./rich-text"; +import { slugify } from "@/lib/format"; export function ArticleForm({ action, defaultValues, }: { action: (formData: FormData) => Promise; + edit?: boolean; defaultValues?: { title?: string; + slug?: string; image?: string; shortStory?: string; fullStory?: string; }; }) { + const t = useTranslations("pages.admin.articles.form"); + 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 suggestedSlug = useMemo(() => slugify(title), [title]); + return ( -
- -
-
+ +
+ + + +
+ +
+ + {t("image")} + +
{ @@ -59,37 +92,44 @@ export function ArticleForm({ src={image} alt="preview" onError={() => setImageError(true)} - style={{ - marginTop: "0.4rem", - maxHeight: 120, - borderRadius: 6, - objectFit: "cover", - }} + className="mt-2 rounded-lg border border-[var(--admin-text-muted)]/14 object-cover" + style={{ maxHeight: 160 }} /> ) : null} {imageError ? ( -

+

Image not found — enter a valid URL or browse.

) : null}
-