Files
Wabenhain/src/app/admin/bewertungen/page.tsx
T
Nick Szaunig c7927f2883 feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
2026-08-17 14:30:28 +02:00

131 lines
5.4 KiB
TypeScript

import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { ConfirmForm } from "@/components/confirm-form";
import { formatDate } from "@/lib/format";
import { insensitiveContains } from "@/lib/prisma-search";
import { approveReviewAction, deleteReviewAction } from "../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminBewertungenPage(props: PageProps<"/admin/bewertungen">) {
const t = getDictionary(await getLocale());
const { q } = await props.searchParams;
const query = typeof q === "string" ? q.trim() : "";
const reviews = await prisma.review.findMany({
where: query
? {
OR: [
{ product: { name: insensitiveContains(query) } },
{ customer: { firstName: insensitiveContains(query) } },
{ customer: { lastName: insensitiveContains(query) } },
],
}
: undefined,
include: {
product: { select: { name: true, slug: true } },
customer: { select: { firstName: true, lastName: true } },
},
orderBy: [{ isApproved: "asc" }, { createdAt: "desc" }],
take: 100,
});
return (
<div>
<BackLink href="/admin" label={t.adminReviewsPage.backToDashboard} />
<h1 className="text-2xl text-forest mb-8">{t.adminReviewsPage.title}</h1>
<form method="GET" className="flex flex-wrap items-end gap-3 mb-6">
<label className="text-sm">
<span className="block text-ink/65 text-xs mb-1">{t.adminReviewsPage.searchLabel}</span>
<input
type="text"
name="q"
defaultValue={query}
placeholder={t.adminReviewsPage.searchPlaceholder}
className="border border-ink/20 bg-background px-3 py-2 text-sm w-64"
/>
</label>
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
{t.adminReviewsPage.filterButton}
</button>
{query ? (
<Link href="/admin/bewertungen" className="text-sm text-ink/65 underline">
{t.adminReviewsPage.resetFilter}
</Link>
) : null}
</form>
<div className="overflow-x-auto bg-background border border-ink/10">
<table className="w-full text-sm">
<thead className="text-left text-ink/65 border-b border-ink/10">
<tr>
<th className="p-4">{t.adminReviewsPage.colProduct}</th>
<th className="p-4">{t.adminReviewsPage.colCustomer}</th>
<th className="p-4">{t.adminReviewsPage.colRating}</th>
<th className="p-4">{t.adminReviewsPage.colComment}</th>
<th className="p-4">{t.adminReviewsPage.colDate}</th>
<th className="p-4">{t.adminReviewsPage.colStatus}</th>
<th className="p-4" />
</tr>
</thead>
<tbody className="divide-y divide-ink/10">
{reviews.map((review) => (
<tr key={review.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4">
<Link href={`/honig/${review.product.slug}`} className="text-forest underline">
{review.product.name}
</Link>
</td>
<td className="p-4">
{review.customer.firstName} {review.customer.lastName}
</td>
<td className="p-4 text-honey-dark whitespace-nowrap">
{"★".repeat(review.rating)}
{"☆".repeat(5 - review.rating)}
</td>
<td className="p-4 max-w-xs">{review.comment ?? "—"}</td>
<td className="p-4 whitespace-nowrap">{formatDate(review.createdAt)}</td>
<td className="p-4">
{review.isApproved ? (
<span className="text-forest">{t.adminReviewsPage.statusPublished}</span>
) : (
<span className="text-honey-dark">{t.adminReviewsPage.statusPending}</span>
)}
</td>
<td className="p-4">
<div className="flex items-center gap-3 whitespace-nowrap">
{!review.isApproved ? (
<form action={approveReviewAction}>
<input type="hidden" name="id" value={review.id} />
<button type="submit" className="text-forest underline">
{t.adminReviewsPage.approve}
</button>
</form>
) : null}
<ConfirmForm
action={deleteReviewAction}
confirmMessage={t.adminReviewsPage.deleteConfirm}
>
<input type="hidden" name="id" value={review.id} />
<button type="submit" className="text-red-700 underline">
{t.adminReviewsPage.delete}
</button>
</ConfirmForm>
</div>
</td>
</tr>
))}
</tbody>
</table>
{reviews.length === 0 ? (
<p className="p-6 text-ink/65">
{query ? t.adminReviewsPage.noResultsFiltered : t.adminReviewsPage.noReviewsYet}
</p>
) : null}
</div>
</div>
);
}