Auth (hand-built, uses the auth core): - /register + register() action: validates, hashes with argon2id (hashPassword), creates an emulator-compatible users row (accountCreated/ipRegister/ipCurrent/ look), redirect kept outside try so NEXT_REDIRECT propagates. Login/register cross-links; header shows Register (logged-out) and a Logout (signOut) button. Batch 5 (parallel agents): /admin/rooms (+[id]) search+detail, /admin/vouchers (CRUD), /admin/subscriptions (read), /admin/calendar (campaigns+rewards read), /marketplace (public). Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0 (52 page routes).
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function AdminRoomDetail({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = await params;
|
|
// rooms is an emulator-owned table → Int id.
|
|
const roomId = Number(id);
|
|
|
|
if (!Number.isInteger(roomId) || roomId <= 0) notFound();
|
|
|
|
let room: Awaited<ReturnType<typeof prisma.rooms.findUnique>> = null;
|
|
try {
|
|
room = await prisma.rooms.findUnique({ where: { id: roomId } });
|
|
} catch {
|
|
room = null;
|
|
}
|
|
if (!room) notFound();
|
|
|
|
const fields: Array<[string, string]> = [
|
|
["ID", String(room.id)],
|
|
["Name", room.name || "(unnamed)"],
|
|
["Description", room.description || "—"],
|
|
["Owner", `${room.ownerName || "—"} (#${room.ownerId})`],
|
|
["State", room.state],
|
|
["Users", `${room.users} / ${room.usersMax}`],
|
|
["Category", String(room.category)],
|
|
["Score", String(room.score)],
|
|
["Model", room.model],
|
|
["Guild ID", room.guildId ? String(room.guildId) : "—"],
|
|
["Tags", room.tags || "—"],
|
|
["Password", room.password ? "yes" : "—"],
|
|
["Public", room.isPublic === "1" ? "yes" : "no"],
|
|
["Staff picked", room.isStaffPicked === "1" ? "yes" : "no"],
|
|
["For sale", room.isForSale === "1" ? "yes" : "no"],
|
|
["Trade mode", String(room.tradeMode)],
|
|
];
|
|
|
|
return (
|
|
<main>
|
|
<p className="muted">
|
|
<Link href="/admin/rooms">← Rooms</Link>
|
|
</p>
|
|
<h1>{room.name || "(unnamed)"}</h1>
|
|
<p className="muted">
|
|
ID {room.id} · owner {room.ownerName || "—"} · {room.users}/{room.usersMax}{" "}
|
|
users · {room.state}
|
|
</p>
|
|
|
|
<div className="card">
|
|
<table>
|
|
<tbody>
|
|
{fields.map(([label, value]) => (
|
|
<tr key={label}>
|
|
<th style={{ width: "35%", whiteSpace: "nowrap" }}>{label}</th>
|
|
<td>{value}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{room.ownerId ? (
|
|
<p className="muted" style={{ marginTop: "1rem" }}>
|
|
<Link href={`/admin/users/${room.ownerId}`}>View owner →</Link>
|
|
</p>
|
|
) : null}
|
|
</main>
|
|
);
|
|
}
|