Files
Epicnabbo-Catalogus-Updated…/src/app/admin/rare-values/page.tsx
T
SimoandCursor 0e89d03940 Finish fine-grained ACL across remaining admin pages and actions.
Replace leftover requireStaff gates with module PERMS, drop hardcoded room rank thresholds, and expand contract tests so admin mutations cannot regress to dashboard-only checks.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:15:22 +02:00

245 lines
7.3 KiB
TypeScript

import { Gem } from "lucide-react";
import { getTranslations } from "next-intl/server";
import {
createCategory,
createValue,
deleteCategory,
deleteValue,
} from "@/actions/admin-rare-values";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"];
export default async function AdminRareValues() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.rareValues");
let categories: Awaited<
ReturnType<typeof prisma.websiteRareValueCategories.findMany>
> = [];
try {
categories = await prisma.websiteRareValueCategories.findMany({
orderBy: [{ priority: "asc" }, { name: "asc" }],
});
} catch {
categories = [];
}
let values: Awaited<ReturnType<typeof prisma.websiteRareValues.findMany>> =
[];
try {
values = await prisma.websiteRareValues.findMany({
orderBy: { name: "asc" },
});
} catch {
values = [];
}
const valuesByCategory = new Map<string, typeof values>();
for (const v of values) {
const key = String(v.categoryId);
const list = valuesByCategory.get(key);
if (list) list.push(v);
else valuesByCategory.set(key, [v]);
}
return (
<main>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
<Gem size={20} className="text-[var(--admin-accent)]" />
</div>
<div>
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">
{t("title")}
</h1>
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">
{t("subtitle")}
</p>
</div>
</div>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
<StatusCard
label={t("categories")}
value={categories.length}
icon="🗂️"
/>
<StatusCard label={t("items")} value={values.length} icon="💎" />
</div>
<form action={createCategory} className="admin-card mb-6">
<h3 className="text-sm font-bold text-[var(--admin-text)] m-0 mb-3">
{t("addCategory")}
</h3>
<div className="flex gap-2 flex-wrap">
<input
name="name"
placeholder={t("categoryForm.namePlaceholder")}
required
className="flex-1 min-w-[160px]"
/>
<input name="badge" placeholder={t("badgePlaceholder")} required />
<input
name="priority"
type="number"
min={1}
defaultValue={1}
placeholder={t("priorityPlaceholder")}
className="w-[110px]"
/>
<button type="submit" className="btn btn-primary">
{t("addCategoryButton")}
</button>
</div>
</form>
{categories.length === 0 ? (
<div className="admin-empty">{t("noCategories")}</div>
) : null}
<div className="grid">
{categories.map((cat) => {
const catId = String(cat.id);
const rows = valuesByCategory.get(catId) ?? [];
return (
<div key={catId} className="admin-card">
<div className="flex gap-3 items-baseline justify-between flex-wrap">
<div
style={{
display: "flex",
gap: "0.6rem",
alignItems: "baseline",
flexWrap: "wrap",
}}
>
<strong>{cat.name}</strong>
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/18 text-[var(--admin-text)]">
{t("badgeLabel", { badge: cat.badge })}
</span>
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/18 text-[var(--admin-text)]">
{t("priorityLabel", { priority: cat.priority })}
</span>
<span className="text-sm theme-text-muted dark:theme-text-muted">
{t("itemCount", { count: rows.length })}
</span>
</div>
<form action={deleteCategory}>
<input type="hidden" name="id" value={catId} />
<button type="submit" className="btn btn-danger">
{t("deleteCategory")}
</button>
</form>
</div>
<table style={{ marginTop: "0.75rem" }}>
<thead>
<tr>
<th>{t("colItemName")}</th>
<th>{t("colItemId")}</th>
<th>{t("colCredits")}</th>
<th>{t("colCurrencyValue")}</th>
<th>{t("colCurrency")}</th>
<th>{t("colIcon")}</th>
<th />
</tr>
</thead>
<tbody>
{rows.map((v) => (
<tr key={String(v.id)}>
<td>{v.name}</td>
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
{v.itemId ?? "—"}
</td>
<td>{v.creditValue ?? "—"}</td>
<td>{v.currencyValue ?? "—"}</td>
<td>
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/18 text-[var(--admin-text)]">
{v.currencyType}
</span>
</td>
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
{v.furnitureIcon}
</td>
<td>
<form action={deleteValue}>
<input type="hidden" name="id" value={String(v.id)} />
<button type="submit" className="btn btn-danger">
✕
</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
{rows.length === 0 ? (
<p
className="text-xs theme-text-muted dark:theme-text-muted"
style={{ margin: "0.5rem 0 0" }}
>
{t("noValuesInCategory")}
</p>
) : null}
<form action={createValue} style={{ marginTop: "0.75rem" }}>
<input type="hidden" name="categoryId" value={catId} />
<div className="flex gap-2 flex-wrap">
<input
name="name"
placeholder={t("itemForm.itemNamePlaceholder")}
required
className="flex-1 min-w-[140px]"
/>
<input
name="itemId"
type="number"
min={1}
placeholder={t("itemForm.itemIdPlaceholder")}
className="w-[110px]"
/>
<input
name="furnitureIcon"
placeholder={t("itemForm.iconPlaceholder")}
required
className="min-w-[140px]"
/>
<input
name="creditValue"
placeholder={t("itemForm.creditsPlaceholder")}
className="w-[130px]"
/>
<input
name="currencyValue"
placeholder={t("itemForm.currencyValuePlaceholder")}
className="w-[130px]"
/>
<select name="currencyType" defaultValue="diamonds">
{CURRENCY_TYPES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<button type="submit" className="btn btn-primary">
{t("itemForm.addValue")}
</button>
</div>
</form>
</div>
);
})}
</div>
</main>
);
}