Fix articles list crash by moving interactive card to client component
Local Build and Deploy / deploy (push) Successful in 1m4s

Extract the article card (thumbnail onError fallback, delete confirmation)
into a Client Component so the admin articles list server page no longer
passes event handlers to server-rendered elements.
This commit is contained in:
openhands committed 2026-07-18 17:45:15 +02:00
1 parent 6215074331
commit 243f8007d9
2 files changed
+88 -61

No files matched your search

+9 -61
View File
@@ -1,8 +1,8 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { deleteArticle } from "@/actions/admin-articles";
import { StatusCard } from "@/components/admin/dashboard";
import { ArticleCard } from "@/components/admin/article-card";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
@@ -94,67 +94,15 @@ export default async function AdminArticles({
) : (
<div className="grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4">
{articles.map((a) => (
<div
<ArticleCard
key={String(a.id)}
className="admin-card overflow-hidden flex flex-col"
>
<div className="relative w-full aspect-[16/9] bg-[var(--admin-canvas)]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={a.image || ""}
alt={a.title}
className="w-full h-full object-cover block"
onError={(
e: React.SyntheticEvent<HTMLImageElement>,
) => {
e.currentTarget.style.visibility = "hidden";
}}
/>
</div>
<div className="p-3 flex flex-col flex-1">
<Link
href={`/admin/articles/${a.id}`}
className="font-semibold text-[var(--admin-text)] hover:text-[var(--admin-accent)] line-clamp-2"
style={{ textDecoration: "none" }}
>
{a.title}
</Link>
<p className="text-xs text-[var(--admin-text-muted)] mt-1 mb-3 flex items-center gap-2 flex-wrap">
<span>{a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "—"}</span>
{authorMap.get(a.userId ?? -1) ? (
<>
<span>·</span>
<span>{authorMap.get(a.userId ?? -1)}</span>
</>
) : null}
</p>
<div className="flex gap-2 mt-auto">
<Link
href={`/admin/articles/${a.id}`}
className="btn btn-primary flex-1 text-center"
>
{t("edit")}
</Link>
<form action={deleteArticle} className="m-0">
<input type="hidden" name="id" value={String(a.id)} />
<button
type="submit"
className="btn btn-danger"
onClick={(e) => {
if (
!window.confirm(
t("confirmDelete", { title: a.title }),
)
)
e.preventDefault();
}}
>
{t("delete")}
</button>
</form>
</div>
</div>
</div>
id={String(a.id)}
title={a.title}
slug={a.slug}
image={a.image}
createdAt={a.createdAt ? a.createdAt.toISOString() : null}
author={authorMap.get(a.userId ?? -1)}
/>
))}
</div>
)}
+79
View File
@@ -0,0 +1,79 @@
"use client";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { deleteArticle } from "@/actions/admin-articles";
export function ArticleCard({
id,
title,
slug,
image,
createdAt,
author,
}: {
id: string;
title: string;
slug: string;
image: string;
createdAt: string | null;
author?: string;
}) {
const t = useTranslations("pages.admin.articles");
return (
<div className="admin-card overflow-hidden flex flex-col">
<div className="relative w-full aspect-[16/9] bg-[var(--admin-canvas)]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={image || ""}
alt={title}
className="w-full h-full object-cover block"
onError={(e) => {
e.currentTarget.style.visibility = "hidden";
}}
/>
</div>
<div className="p-3 flex flex-col flex-1">
<Link
href={`/admin/articles/${id}`}
className="font-semibold text-[var(--admin-text)] hover:text-[var(--admin-accent)] line-clamp-2"
style={{ textDecoration: "none" }}
>
{title}
</Link>
<p className="text-xs text-[var(--admin-text-muted)] mt-1 mb-3 flex items-center gap-2 flex-wrap">
<span>{createdAt ? createdAt.slice(0, 10) : "—"}</span>
{author ? (
<>
<span>·</span>
<span>{author}</span>
</>
) : null}
</p>
<div className="flex gap-2 mt-auto">
<Link
href={`/admin/articles/${id}`}
className="btn btn-primary flex-1 text-center"
>
{t("edit")}
</Link>
<form action={deleteArticle} className="m-0">
<input type="hidden" name="id" value={id} />
<button
type="submit"
className="btn btn-danger"
onClick={(e) => {
if (!window.confirm(t("confirmDelete", { title }))) {
e.preventDefault();
}
}}
>
{t("delete")}
</button>
</form>
</div>
</div>
</div>
);
}