● Zion Boggan zionboggan.com ↗

Add admin Reports queue so community flags get reviewed instead of vanishing

b5f46b7   Zion Boggan committed on Jul 4, 2026 (1 month ago)
apps/web/src/app/admin/page.tsx +64 -0
@@ -31,6 +31,7 @@ export default async function AdminPage() {
const launchedCities = await caller.city.listLaunched();
const pendingPhotos = await caller.eventPhoto.adminPending();
const vendorList = await caller.vendor.adminList();
+ const openFlags = await caller.flag.adminOpen();
const money = (c: number) => (c === 0 ? "Free" : `$${(c / 100).toFixed(2)}`);
@@ -81,6 +82,19 @@ export default async function AdminPage() {
revalidatePath("/admin");
}
+ async function resolveFlag(formData: FormData) {
+ "use server";
+ const id = String(formData.get("id") ?? "");
+ const status = String(formData.get("status") ?? "");
+ if (!id || !["reviewed", "actioned", "dismissed"].includes(status)) return;
+ const { caller } = await sessionCaller();
+ await caller.flag.adminResolve({
+ id,
+ status: status as "reviewed" | "actioned" | "dismissed",
+ });
+ revalidatePath("/admin");
+ }
+
async function createVendor(formData: FormData) {
"use server";
const cityId = String(formData.get("cityId") ?? "");
@@ -253,6 +267,56 @@ export default async function AdminPage() {
</ul>
</section>
+ {/* Reports: open community flags wait here for review */}
+ <section className="mt-14 border-t border-[var(--line)] pt-8">
+ <div className="flex items-end justify-between">
+ <h2 className="display text-3xl">Reports</h2>
+ <span className="kicker text-ash">Open community reports</span>
+ </div>
+ <ul className="mt-6">
+ {openFlags.length === 0 ? (
+ <li className="kicker text-ash">Nothing waiting for review.</li>
+ ) : (
+ openFlags.map((f) => (
+ <li
+ key={f.id}
+ className="flex items-center gap-4 border-b border-[var(--line)] py-4"
+ >
+ <div className="flex-1">
+ <p className="display text-lg text-bone">
+ {f.subjectType.replace(/_/g, " ")}
+ </p>
+ <p className="kicker mt-1 text-ash">
+ {f.reason || "No reason given"} · {f.subjectId.slice(0, 8)}
+ </p>
+ </div>
+ <span className="kicker hidden text-ash sm:block">{stamp(f.createdAt)}</span>
+ <form action={resolveFlag} className="flex gap-3">
+ <input type="hidden" name="id" value={f.id} />
+ <button
+ name="status"
+ value="actioned"
+ className="kicker text-gold hover:text-gold-bright"
+ >
+ Actioned
+ </button>
+ <button
+ name="status"
+ value="reviewed"
+ className="kicker text-bone hover:text-gold"
+ >
+ Reviewed
+ </button>
+ <button name="status" value="dismissed" className="kicker text-ash hover:text-bone">
+ Dismiss
+ </button>
+ </form>
+ </li>
+ ))
+ )}
+ </ul>
+ </section>
+
{/* Event media [F1/F4]: attach a flyer image or PDF program */}
<section className="mt-14 border-t border-[var(--line)] pt-8">
<div className="flex items-end justify-between">
packages/api/src/root.ts +18 -0
@@ -527,6 +527,24 @@ export const appRouter = router({
.returning();
return row;
}),
+ adminOpen: adminProcedure.query(({ ctx }) =>
+ ctx.db
+ .select()
+ .from(flags)
+ .where(eq(flags.status, "open"))
+ .orderBy(desc(flags.createdAt))
+ .limit(100),
+ ),
+ adminResolve: adminProcedure
+ .input(z.object({ id: uuid, status: z.enum(["reviewed", "actioned", "dismissed"]) }))
+ .mutation(async ({ ctx, input }) => {
+ const [row] = await ctx.db
+ .update(flags)
+ .set({ status: input.status })
+ .where(eq(flags.id, input.id))
+ .returning();
+ return row;
+ }),
}),
// ---- Directories: promoters / DJs / stepping groups / classes ------------