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

+87
View File
@@ -0,0 +1,87 @@
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
type Achievement = {
name: string;
category: string;
level: number;
rewardAmount: number;
rewardType: number;
points: number | null;
progressNeeded: number;
};
export default async function AdminAchievements() {
let achievements: Achievement[] = [];
try {
achievements = await prisma.achievements.findMany({
select: {
name: true,
category: true,
level: true,
rewardAmount: true,
rewardType: true,
points: true,
progressNeeded: true,
},
orderBy: [{ category: "asc" }, { name: "asc" }, { level: "asc" }],
});
} catch {
achievements = [];
}
// Group by category (already sorted by category, then name, then level).
const groups = new Map<string, Achievement[]>();
for (const a of achievements) {
const list = groups.get(a.category) ?? [];
list.push(a);
groups.set(a.category, list);
}
return (
<main>
<h1>Achievements</h1>
<p className="muted" style={{ marginTop: 0 }}>
Read-only · {achievements.length} rows across {groups.size} categories.
</p>
{groups.size === 0 ? (
<p className="muted">No achievements found.</p>
) : (
[...groups.entries()].map(([category, rows]) => (
<div key={category} className="card" style={{ marginBottom: "1.5rem" }}>
<h3 style={{ marginTop: 0 }}>{category}</h3>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ textAlign: "left", color: "var(--muted)" }}>
<th>Name</th>
<th>Level</th>
<th>Progress needed</th>
<th>Reward type</th>
<th>Reward amount</th>
<th>Points</th>
</tr>
</thead>
<tbody>
{rows.map((a) => (
<tr
key={`${a.name}-${a.level}`}
style={{ borderTop: "1px solid var(--border)" }}
>
<td>{a.name}</td>
<td>{a.level}</td>
<td>{a.progressNeeded}</td>
<td>{a.rewardType}</td>
<td>{a.rewardAmount}</td>
<td>{a.points ?? 0}</td>
</tr>
))}
</tbody>
</table>
</div>
))
)}
</main>
);
}