"use client" import { useEffect, useMemo, useRef, useState } from "react" import Image from "next/image" import { Loader2, SearchIcon, ShieldCheckIcon, XIcon, ChevronLeftIcon, ChevronRightIcon, FlagIcon, } from "lucide-react" import { ConfirmDialog } from "@/components/ui/modal" interface Report { id: string entryId: string reporterId: string reporterName: string | null reason: "inaccurate" | "spam" | "inappropriate" | "other" details: string | null status: "open" | "reviewed" | "dismissed" createdAt: string entry: { id: string userId: string fpsAvg: number | null fpsLow: number | null fpsHigh: number | null upscalerType: string | null userNotes: string | null isRemoved: boolean authorName: string | null } gameVersion: { id: string versionString: string } game: { id: string title: string headerImage: string | null } } interface ReportsApiResponse { data: Report[] total: number limit: number offset: number } type StatusFilter = "all" | "open" | "reviewed" | "dismissed" const LIMIT = 20 function formatDate(value: string | Date | null | undefined) { if (!value) return "—" const d = typeof value === "string" ? new Date(value) : value return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) } function getInitial(name: string | null | undefined) { return name?.charAt(0)?.toUpperCase() || "?" } function truncate(str: string | null | undefined, maxLen = 60) { if (!str) return "—" return str.length > maxLen ? str.slice(0, maxLen) + "…" : str } function statusBadgeClasses(status: Report["status"]) { switch (status) { case "open": return "bg-yellow-500/10 text-yellow-400" case "reviewed": return "bg-green-500/10 text-green-400" case "dismissed": return "bg-text/5 text-text/50" } } function reasonBadgeClasses(reason: Report["reason"]) { switch (reason) { case "inaccurate": return "bg-blue-500/10 text-blue-400" case "spam": return "bg-red-500/10 text-red-400" case "inappropriate": return "bg-orange-500/10 text-orange-400" case "other": return "bg-text/5 text-text/50" } } export function ReportsClient() { const [reports, setReports] = useState([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) const [search, setSearch] = useState("") const [offset, setOffset] = useState(0) const [statusFilter, setStatusFilter] = useState("all") const [actionLoading, setActionLoading] = useState>({}) const [confirmReport, setConfirmReport] = useState(null) const isSearchChangeRef = useRef(false) const handleSearchChange = (value: string) => { setSearch(value) setOffset(0) isSearchChangeRef.current = true } const handleStatusChange = (value: StatusFilter) => { setStatusFilter(value) setOffset(0) isSearchChangeRef.current = true } const handlePrev = () => { setOffset((prev) => Math.max(0, prev - LIMIT)) isSearchChangeRef.current = false } const handleNext = () => { setOffset((prev) => prev + LIMIT) isSearchChangeRef.current = false } useEffect(() => { const delay = isSearchChangeRef.current ? 300 : 0 isSearchChangeRef.current = false let cancelled = false const timer = setTimeout(async () => { setLoading(true) try { const params = new URLSearchParams({ limit: String(LIMIT), offset: String(offset), }) if (statusFilter !== "all") { params.set("status", statusFilter) } const res = await fetch(`/api/admin/reports?${params.toString()}`) if (res.ok && !cancelled) { const json = (await res.json()) as ReportsApiResponse setReports(json.data) setTotal(json.total) } } catch { // ignore } finally { if (!cancelled) setLoading(false) } }, delay) return () => { cancelled = true clearTimeout(timer) } }, [search, offset, statusFilter]) const handleUpdateStatus = async (report: Report, status: "reviewed" | "dismissed") => { setActionLoading((prev) => ({ ...prev, [report.id]: true })) try { const res = await fetch(`/api/admin/reports/${report.id}/status`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }) if (res.ok) { // Refresh list after action const params = new URLSearchParams({ limit: String(LIMIT), offset: String(offset), }) if (statusFilter !== "all") { params.set("status", statusFilter) } const listRes = await fetch(`/api/admin/reports?${params.toString()}`) if (listRes.ok) { const json = (await listRes.json()) as ReportsApiResponse setReports(json.data) setTotal(json.total) } setConfirmReport(null) } } finally { setActionLoading((prev) => ({ ...prev, [report.id]: false })) } } const filteredReports = useMemo(() => { const term = search.trim().toLowerCase() if (!term) return reports return reports.filter( (r) => r.reporterName?.toLowerCase().includes(term) || r.game.title.toLowerCase().includes(term) || r.reason.toLowerCase().includes(term) || r.details?.toLowerCase().includes(term) || r.status.toLowerCase().includes(term) ) }, [reports, search]) const hasMore = offset + filteredReports.length < total const tabs: { label: string; value: StatusFilter }[] = [ { label: "All", value: "all" }, { label: "Open", value: "open" }, { label: "Reviewed", value: "reviewed" }, { label: "Dismissed", value: "dismissed" }, ] return (
{/* Header */}

Reports

{/* Search */}
handleSearchChange(e.target.value)} placeholder="Search reports..." className="w-full pl-9 pr-4 py-2 rounded-md bg-text/5 border border-border text-sm text-text placeholder:text-text/40 focus:outline-none focus:border-primary/60 transition-colors" />
{/* Filter tabs */}
{tabs.map((tab) => ( ))}
{/* Table */}
{loading ? ( ) : filteredReports.length === 0 ? ( ) : ( filteredReports.map((report) => ( )) )}
Reporter Game FPS Reason Details Status Date Actions
No reports found.
{getInitial(report.reporterName)}

{report.reporterName || "Unknown"}

{report.game.headerImage ? ( {report.game.title} ) : null}

{report.game.title}

{report.entry.fpsAvg ?? "—"} {report.reason}

{truncate(report.details, 80)}

{report.status} {formatDate(report.createdAt)}
{report.status === "open" ? ( <> ) : ( {report.status} )}
{/* Pagination */} {filteredReports.length > 0 && (

Showing {offset + 1}–{Math.min(offset + filteredReports.length, total)} of{" "} {total}

)} {/* Confirmation Dialog */} {confirmReport && ( setConfirmReport(null)} onConfirm={() => handleUpdateStatus(confirmReport, "reviewed")} title="Confirm Review" message="This will also remove the reported benchmark. Are you sure?" confirmLabel="Review" variant="default" loading={actionLoading[confirmReport.id]} /> )}
) }