| | @@ -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"> |