Add niche admin + public expansions + self-host Nunito font (batches 3-4)
Built via two more parallel agent workflows (read schema -> return files), integrated + verified. Admin: /admin/emulator (emulator_settings + emulator_texts key/value editor), /admin/badges (give-badge via RCON), /admin/rare-values (CRUD), /admin/housekeeping (CRUD), /admin/email-templates (CRUD), /admin/photos (moderation). Public: /rares (+[category]), /leaderboard (credits/diamonds/duckets), /guilds (+[id] members), /redeem (voucher -> sendCurrency). Expanded: /u/[username] now shows badges + photos + guestbook (post form); /news/[slug] now shows reactions + comments (comment form). Reactions tallied in JS (Prisma groupBy typing avoided). Self-hosted Nunito via next/font/google wired to --font-nunito (the atom theme font, no runtime external fetch). Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0.
This commit is contained in:
1 parent
e96b606e1e
commit
e8be0461d8
28 files changed
+2347
-5
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import { deletePhoto } from "@/actions/admin-photos";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminPhotos() {
|
||||
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = [];
|
||||
try {
|
||||
photos = await prisma.cameraWeb.findMany({
|
||||
orderBy: { timestamp: "desc" },
|
||||
take: 60,
|
||||
});
|
||||
} catch {
|
||||
photos = [];
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Photo Moderation</h1>
|
||||
<p className="muted">
|
||||
Recent in-game camera photos. Deleting removes the record from{" "}
|
||||
<code>camera_web</code>.
|
||||
</p>
|
||||
|
||||
<div className="grid cols-3">
|
||||
{photos.map((p) => (
|
||||
<div key={String(p.id)} className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={p.url}
|
||||
alt={`Photo ${String(p.id)}`}
|
||||
style={{ width: "100%", height: 200, objectFit: "cover", display: "block", background: "#f1f5f9" }}
|
||||
/>
|
||||
<div style={{ padding: "0.85rem 1.05rem 1.05rem" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem", alignItems: "baseline" }}>
|
||||
<strong>#{String(p.id)}</strong>
|
||||
<span className="muted">Room {p.roomId}</span>
|
||||
</div>
|
||||
<p className="muted" style={{ margin: "0.3rem 0 0" }}>
|
||||
User {p.userId} ·{" "}
|
||||
{new Date(p.timestamp * 1000).toISOString().slice(0, 16).replace("T", " ")}
|
||||
</p>
|
||||
<form action={deletePhoto} style={{ marginTop: "0.75rem" }}>
|
||||
<input type="hidden" name="id" value={String(p.id)} />
|
||||
<button type="submit" className="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{photos.length === 0 ? <p className="muted">No photos found.</p> : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user