Add remaining public + admin pages (parallel build, 26 files)

Built via two parallel agent workflows reading the real Prisma schema, then
integrated + verified.

Public: /shop (categories + storefront), /community hub, /rankings (top by
credits), /staff, /photos (camera_web gallery), /help (categories + rules),
/help/tickets (auth-gated user tickets + create), /settings (auth-gated, update
motto via RCON).

Admin: /admin/catalog (+[id] items), /admin/radio (shouts/apps/schedules +
delete shout), /admin/teams (CRUD), /admin/permissions (read), /admin/wordfilter
(CRUD + RCON push), /admin/ip (whitelist/blacklist CRUD), /admin/applications
(list + dismiss), /admin/logs (chat/command/alert read), /admin/achievements
(read). Server actions all staff-gated.

Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build
exit 0 (33 routes); curl-probed every route — public 200/<main>, auth+admin
correctly 307-redirect when unauthorized.
This commit is contained in:
Simo committed 2026-06-28 13:24:55 +02:00
1 parent 9a79acebe7
commit e96b606e1e
27 files changed
+2400

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
import { redirect } from "next/navigation";
import { createTicket } from "@/actions/help-tickets";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
function formatDate(d: Date | null): string {
return d ? d.toISOString().slice(0, 16).replace("T", " ") : "";
}
export default async function HelpTicketsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const userId = Number(session.user.id);
let tickets: Awaited<ReturnType<typeof prisma.websiteHelpCenterTickets.findMany>> = [];
try {
tickets = await prisma.websiteHelpCenterTickets.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
take: 100,
});
} catch {
tickets = [];
}
return (
<main>
<h1>Help Tickets</h1>
<p className="muted">Submit a ticket to staff and track your open requests.</p>
<form action={createTicket} className="card" style={{ marginBottom: "1.5rem" }}>
<h3 style={{ marginTop: 0 }}>New ticket</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "0.6rem" }}>
<input name="title" placeholder="Subject" maxLength={255} required />
<textarea
name="content"
placeholder="Describe your issue…"
rows={5}
maxLength={5000}
required
/>
<div>
<button type="submit" className="btn btn-primary">
Submit ticket
</button>
</div>
</div>
</form>
<table>
<thead>
<tr>
<th>ID</th>
<th>Subject</th>
<th>Status</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{tickets.map((t) => (
<tr key={String(t.id)}>
<td>{String(t.id)}</td>
<td>{t.title}</td>
<td>{t.open ? "Open" : "Closed"}</td>
<td className="muted">{formatDate(t.createdAt)}</td>
</tr>
))}
</tbody>
</table>
{tickets.length === 0 ? <p className="muted">You have no tickets yet.</p> : null}
</main>
);
}