SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
4 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
2.5 KB · 16 lines tsx
Raw Blame History
1"use client";2import { useEffect, useState } from "react";3import { fmtDate } from "@/lib/format";4type R = { id: string; targetType: string; targetId: string; reason: string; details: string | null; status: string; createdAt: string };5export function AdminReports() {6  const [status, setStatus] = useState("OPEN"); const [rows, setRows] = useState<R[]>([]); const load = () => fetch(`/api/admin/reports?status=${status}`).then((r) => r.json()).then((j) => setRows(j.items ?? []));7  useEffect(() => { load(); }, [status]); // eslint-disable-line react-hooks/exhaustive-deps8  const act = async (id: string, s: string) => { await fetch("/api/admin/reports", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id, status: s }) }); load(); };9  const link = (r: R) => r.targetType === "AGGREGATED_LISTING" ? `/annonce/${encodeURIComponent(r.targetId)}` : r.targetType === "LISTING" ? `/admin/annonces?id=${r.targetId}` : null;10  return (11    <div><div className="flex items-center gap-2 mb-3"><h1 className="text-2xl font-extrabold tracking-tight">Signalements</h1><select value={status} onChange={(e) => setStatus(e.target.value)} className="tv-chip appearance-none ml-auto"><option value="OPEN">Ouverts</option><option value="RESOLVED">Résolus</option><option value="DISMISSED">Rejetés</option></select></div>12      <ul className="tv-card divide-y divide-line text-sm">{rows.map((r) => <li key={r.id} className="p-3 flex flex-wrap gap-3 items-center"><div className="min-w-0 flex-1"><p className="font-semibold">{r.reason.replace(/_/g, " ")} <span className="text-ink-3 font-normal">· {r.targetType} · {fmtDate(r.createdAt)}</span></p><p className="text-xs text-ink-2 break-all">{r.targetId}</p>{r.details && <p className="text-xs text-ink-2 mt-1">« {r.details} »</p>}</div><div className="flex gap-2 text-xs">{link(r) && <a href={link(r)!} target="_blank" rel="noreferrer" className="underline">Voir la cible</a>}{r.status === "OPEN" && <><button type="button" onClick={() => act(r.id, "RESOLVED")} className="tv-btn tv-btn-primary !py-1 !px-3">Résolu</button><button type="button" onClick={() => act(r.id, "DISMISSED")} className="tv-btn tv-btn-ghost !py-1 !px-3">Sans suite</button></>}</div></li>)}{rows.length === 0 && <li className="p-6 text-center text-ink-3">Aucun signalement.</li>}</ul>13      <p className="text-xs text-ink-3 mt-2">Pour une annonce agrégée, « Résolu » signifie que la demande a été transmise/traitée côté source ; l'annonce disparaîtra à la prochaine synchronisation si la source la retire.</p></div>14  );15}16