diff --git a/app/(dashboard)/reports/page.tsx b/app/(dashboard)/reports/page.tsx index 3851744..ea97886 100644 --- a/app/(dashboard)/reports/page.tsx +++ b/app/(dashboard)/reports/page.tsx @@ -7,6 +7,8 @@ import { usePagedList } from "@/lib/use-paged-list"; import { ListShell, EmptyRow } from "@/components/list-shell"; import { Table, THead, TR, TH, TD } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; +import { Modal } from "@/components/ui/modal"; +import { Textarea } from "@/components/ui/input"; import { useConfirm } from "@/components/confirm"; import { cn, fmtDate } from "@/lib/utils"; @@ -25,6 +27,7 @@ export default function ReportsPage() { ); const list = usePagedList(fetcher, [type, q]); const confirm = useConfirm(); + const [resolveTarget, setResolveTarget] = useState(null); async function run(action: Promise, successMsg: string) { try { @@ -36,8 +39,9 @@ export default function ReportsPage() { } } - // Per-report moderation buttons. Destructive actions confirm first. - function actionsFor(r: AdminReport) { + // The moderation button when the target is still live. Destructive actions + // confirm first. (When the target is already removed we show "Undo" instead.) + function modButton(r: AdminReport) { if (r.type === "post") { return ( <> @@ -51,8 +55,7 @@ export default function ReportsPage() { if ( await confirm({ title: "Delete this post?", - message: - "It will be removed from the app. You can restore it later from the Posts tab.", + message: "It will be removed from the app. You can undo this.", confirmLabel: "Delete post", destructive: true, }) @@ -75,7 +78,7 @@ export default function ReportsPage() { if ( await confirm({ title: "Delete this comment?", - message: "It will be removed from the thread (restorable).", + message: "It will be removed from the thread. You can undo this.", confirmLabel: "Delete comment", destructive: true, }) @@ -96,7 +99,7 @@ export default function ReportsPage() { if ( await confirm({ title: "Suspend this account?", - message: "They'll be signed out and unable to log in until you unban them.", + message: "They'll be signed out and unable to log in. You can undo this.", confirmLabel: "Suspend account", destructive: true, }) @@ -110,6 +113,25 @@ export default function ReportsPage() { ); } + // Shown when the reported content is currently removed (deleted / banned) — + // reverts the soft-deletion or ban. + function undoButton(r: AdminReport) { + return ( + + ); + } + const targetLabel = type === "user" ? "Reported account" : type === "comment" ? "Comment" : "Post"; @@ -153,61 +175,139 @@ export default function ReportsPage() { {list.items.length === 0 && !list.loading ? ( ) : ( - list.items.map((r) => ( - - - {type === "user" ? ( - - {r.target_text ? `@${r.target_text}` : "—"} - - ) : ( - <> -
{r.target_text || "—"}
- {r.target_author_handle ? ( -
- by @{r.target_author_handle} -
- ) : null} - - )} - - -
- {r.category} -
- {r.reason ? ( -
{r.reason}
- ) : null} - - - {r.reporter_handle ? `@${r.reporter_handle}` : "—"} - - {fmtDate(r.created_at)} - -
- {actionsFor(r)} - {r.resolved_at ? ( - - Resolved + list.items.map((r) => { + const resolved = !!r.resolved_at; + return ( + + + {type === "user" ? ( + + {r.target_text ? `@${r.target_text}` : "—"} ) : ( - + <> +
{r.target_text || "—"}
+ {r.target_author_handle ? ( +
+ by @{r.target_author_handle} +
+ ) : null} + )} -
- - - )) + {r.target_removed ? ( +
+ {r.type === "user" ? "Suspended" : "Removed"} +
+ ) : null} + + +
+ {r.category} +
+ {r.reason ? ( +
{r.reason}
+ ) : null} + + + {r.reporter_handle ? `@${r.reporter_handle}` : "—"} + + + {fmtDate(r.created_at)} + + + {resolved ? ( +
+ + ✓ Resolved + + {r.resolution_note ? ( +

+ {r.resolution_note} +

+ ) : null} + {r.target_removed ? undoButton(r) : null} +
+ ) : ( +
+ {r.target_removed ? undoButton(r) : modButton(r)} + +
+ )} + + + ); + }) )} + + setResolveTarget(null)} + onDone={() => { + setResolveTarget(null); + list.refresh(); + }} + /> ); } + +function ResolveModal({ + report, + onClose, + onDone, +}: { + report: AdminReport | null; + onClose: () => void; + onDone: () => void; +}) { + const [note, setNote] = useState(""); + const [busy, setBusy] = useState(false); + + async function submit() { + if (!report || note.trim() === "") return; + setBusy(true); + try { + await api.resolveReport(report.type, report.id, note.trim()); + toast.success("Report resolved."); + setNote(""); + onDone(); + } catch (err) { + toast.error(err instanceof AdminError ? err.message : "Could not resolve."); + } finally { + setBusy(false); + } + } + + return ( + +
+