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 U = { id: string; email: string; displayName: string | null; status: string; createdAt: string; roles: string[]; nListings: number };5export function AdminUsers() {6  const [q, setQ] = useState(""); const [rows, setRows] = useState<U[]>([]); const load = () => fetch(`/api/admin/users?q=${encodeURIComponent(q)}`).then((r) => r.json()).then((j) => setRows(j.items ?? []));7  useEffect(() => { const t = setTimeout(load, 250); return () => clearTimeout(t); }, [q]); // eslint-disable-line react-hooks/exhaustive-deps8  const act = async (id: string, action: string, role?: string) => { await fetch("/api/admin/users", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id, action, role }) }); load(); };9  return (10    <div><div className="flex items-center gap-2 mb-3"><h1 className="text-2xl font-extrabold tracking-tight">Utilisateurs</h1><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Rechercher un courriel" className="tv-chip ml-auto !pl-3" /></div>11      <ul className="tv-card divide-y divide-line text-sm">{rows.map((u) => <li key={u.id} className="p-3 flex flex-wrap gap-3 items-center"><div className="min-w-0 flex-1"><p className="font-semibold truncate">{u.email} {u.status !== "ACTIVE" && <span className="tv-badge bg-warn-soft text-warn">{u.status}</span>}</p><p className="text-xs text-ink-2">{u.displayName ?? "—"} · inscrit le {fmtDate(u.createdAt)} · {u.nListings} annonce(s) · rôles : {u.roles.length ? u.roles.join(", ") : "aucun"}</p></div>12        <div className="flex flex-wrap gap-2 text-xs">{u.status === "ACTIVE" ? <button type="button" onClick={() => confirm("Suspendre ce compte ?") && act(u.id, "suspend")} className="tv-btn tv-btn-ghost !py-1 !px-3">Suspendre</button> : <button type="button" onClick={() => act(u.id, "activate")} className="tv-btn tv-btn-ghost !py-1 !px-3">Réactiver</button>}{["MODERATOR", "AGENT_VERIFIED", "ADMIN"].map((r) => u.roles.includes(r) ? <button key={r} type="button" onClick={() => act(u.id, "revoke", r)} className="underline text-ink-3">− {r}</button> : <button key={r} type="button" onClick={() => confirm(`Attribuer le rôle ${r} ?`) && act(u.id, "grant", r)} className="underline">+ {r}</button>)}</div></li>)}</ul>13      <p className="text-xs text-ink-3 mt-2">AGENT_VERIFIED n'est attribué qu'après vérification hors ligne (permis OACIQ, mandat) ; ADMIN exige un TOTP actif.</p></div>14  );15}16