TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import { useEffect, useState } from "react";3import { fmtPrice, fmtDate } from "@/lib/format";4import { STATUS_LABEL } from "@/lib/listings/types";5type Row = { l: { id: string; slug: string; title: string | null; status: string; listingType: string; priceAmount: string | null; moderationNote: string | null; updatedAt: string; sourceType: string }; p: { city: string | null; canonicalAddress: string | null; propertyType: string }; ownerEmail: string | null };6export function AdminListings() {7 const [status, setStatus] = useState("PENDING_REVIEW"); const [rows, setRows] = useState<Row[]>([]); const load = () => fetch(`/api/admin/listings?status=${status}`).then((r) => r.json()).then((j) => setRows(j.items ?? []));8 useEffect(() => { load(); }, [status]); // eslint-disable-line react-hooks/exhaustive-deps9 const act = async (id: string, action: string) => { const note = action !== "approve" ? prompt("Motif communiqué à l'annonceur :") ?? undefined : undefined; if (action !== "approve" && note == null) return; await fetch("/api/admin/listings", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id, action, note }) }); load(); };10 return (11 <div>12 <div className="flex items-center gap-2 mb-3"><h1 className="text-2xl font-extrabold tracking-tight">Annonces</h1><select value={status} onChange={(e) => setStatus(e.target.value)} className="tv-chip appearance-none ml-auto">{["PENDING_REVIEW", "PUBLISHED", "REJECTED", "PAUSED", "DRAFT", "all"].map((s) => <option key={s} value={s}>{STATUS_LABEL[s] ?? "Toutes"}</option>)}</select></div>13 <ul className="tv-card divide-y divide-line text-sm">{rows.map(({ l, p, ownerEmail }) => <li key={l.id} className="p-3 flex flex-wrap gap-3 items-center"><div className="min-w-0 flex-1"><p className="font-semibold truncate">{l.title ?? "(sans titre)"} <span className="text-ink-3 font-normal">· {l.listingType === "RENT" ? "location" : "vente"} · {p.propertyType} · {fmtPrice(l.priceAmount ? Number(l.priceAmount) : null, { rent: l.listingType === "RENT" })}</span></p><p className="text-xs text-ink-2 truncate">{p.canonicalAddress}, {p.city} · {ownerEmail ?? "—"} · {l.sourceType} · maj {fmtDate(l.updatedAt)}</p>{l.moderationNote && <p className="text-xs text-warn">{l.moderationNote}</p>}</div>14 <div className="flex gap-2 text-xs"><a href={`/annonce/${encodeURIComponent("toveli:" + l.slug)}`} target="_blank" rel="noreferrer" className="underline">Voir</a><a href={`/membre/annonces/${l.id}`} className="underline">Éditer</a>{l.status !== "PUBLISHED" && <button type="button" onClick={() => act(l.id, "approve")} className="tv-btn tv-btn-primary !py-1 !px-3">Approuver</button>}{l.status === "PUBLISHED" && <button type="button" onClick={() => act(l.id, "unpublish")} className="tv-btn tv-btn-ghost !py-1 !px-3">Retirer</button>}{l.status !== "REJECTED" && <button type="button" onClick={() => act(l.id, "reject")} className="tv-btn tv-btn-ghost !py-1 !px-3 !text-brand-dark">Refuser</button>}</div></li>)}{rows.length === 0 && <li className="p-6 text-center text-ink-3">Rien à afficher.</li>}</ul>15 </div>16 );17}18