Files
la-voix-du-peuple/artifacts/voix-du-peuple/src/pages/contributions-brutes.tsx
T
billisdead bae1335dd5 Remove internal Gitea URLs, personal email, and add AI prompts to site
- Replace all homegit.gyozamancave.fr links with internal page links
  or plain text (no external repo exposure)
- Replace piron.antoine@gmail.com with contact@example.com placeholder
- Add prompts verbatim section to /transparence page (filter, synthesis,
  law-check prompts) via new src/lib/prompts.ts module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 07:54:36 +02:00

193 lines
7.0 KiB
TypeScript

// Copyright (C) 2026 billisdead — Licence EUPL-1.2
import React from "react";
import { Link } from "wouter";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
import {
ArrowLeft, Download, ChevronLeft, ChevronRight, Loader2, AlertCircle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
const API_BASE = import.meta.env.VITE_API_URL ?? "";
const PER_PAGE = 20;
interface Contribution {
id: number;
content: string;
author: string | null;
createdAt: string;
}
interface ContributionsResponse {
contributions: Contribution[];
total: number;
page: number;
perPage: number;
pages: number;
}
export default function ContributionsBrutes() {
const [page, setPage] = React.useState(1);
const [data, setData] = React.useState<ContributionsResponse | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(false);
React.useEffect(() => {
setLoading(true);
setError(false);
fetch(`${API_BASE}/api/contributions?page=${page}&per_page=${PER_PAGE}`)
.then((res) => {
if (!res.ok) throw new Error();
return res.json() as Promise<ContributionsResponse>;
})
.then((d) => {
setData(d);
setLoading(false);
})
.catch(() => {
setError(true);
setLoading(false);
});
}, [page]);
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto max-w-3xl px-4 md:px-8 py-12">
<Link href="/">
<Button variant="ghost" className="mb-8 -ml-3 text-muted-foreground">
<ArrowLeft className="mr-2 h-4 w-4" /> Retour
</Button>
</Link>
<header className="mb-8 space-y-3">
<h1 className="text-4xl md:text-5xl font-serif font-bold text-primary">
Contributions brutes
</h1>
<p className="text-muted-foreground text-sm leading-relaxed">
L'intégralité des contributions acceptées, dans leur ordre de soumission, sans mise en forme éditoriale.
</p>
</header>
{/* Avertissement éditorial */}
<Alert className="mb-8 border-amber-200/70 bg-amber-50/50 dark:bg-amber-950/20 dark:border-amber-800/40">
<AlertDescription className="text-sm text-foreground/80 leading-relaxed">
La{" "}
<Link href="/" className="underline text-primary">
synthèse collective
</Link>{" "}
visible sur la page d'accueil est une interprétation thématique automatisée de ces contributions.
Elle peut regrouper, omettre ou reformuler des expressions individuelles.
Cette page est la source primaire pour vérification.
</AlertDescription>
</Alert>
{/* Boutons d'export */}
<div className="flex gap-3 mb-8">
<a
href={`${API_BASE}/api/contributions/export/json`}
download="contributions.json"
>
<Button variant="outline" size="sm" className="gap-2 font-mono text-xs">
<Download className="h-3.5 w-3.5" /> Export JSON
</Button>
</a>
<a
href={`${API_BASE}/api/contributions/export/csv`}
download="contributions.csv"
>
<Button variant="outline" size="sm" className="gap-2 font-mono text-xs">
<Download className="h-3.5 w-3.5" /> Export CSV
</Button>
</a>
</div>
{/* Contenu */}
{loading ? (
<div className="flex justify-center py-16 text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : error ? (
<div className="flex flex-col items-center gap-3 py-16 text-muted-foreground">
<AlertCircle className="h-6 w-6 text-destructive" />
<span className="text-sm">Impossible de charger les contributions.</span>
</div>
) : data && data.contributions.length > 0 ? (
<>
<p className="text-xs font-mono text-muted-foreground mb-6">
{data.total} contribution{data.total !== 1 ? "s" : ""} acceptée{data.total !== 1 ? "s" : ""}
{" "} page {data.page} / {data.pages}
</p>
<div className="space-y-6">
{data.contributions.map((c) => (
<div
key={c.id}
className="border-l-2 border-border/50 pl-4 space-y-1"
id={`contrib-${c.id}`}
>
<p className="text-sm leading-relaxed font-serif text-foreground/90">
{c.content}
</p>
<div className="flex items-center gap-3 text-xs font-mono text-muted-foreground">
<span className="font-semibold text-primary/70">
{c.author || "Citoyen anonyme"}
</span>
<span>&bull;</span>
<span>
{format(new Date(c.createdAt), "d MMMM yyyy, HH:mm", { locale: fr })}
</span>
<span className="ml-auto opacity-40">#{c.id}</span>
</div>
</div>
))}
</div>
{/* Pagination */}
{data.pages > 1 && (
<div className="flex items-center justify-between mt-10 pt-6 border-t border-border/40">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="gap-1.5"
>
<ChevronLeft className="h-4 w-4" /> Précédent
</Button>
<span className="text-xs font-mono text-muted-foreground">
{page} / {data.pages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(data.pages, p + 1))}
disabled={page === data.pages}
className="gap-1.5"
>
Suivant <ChevronRight className="h-4 w-4" />
</Button>
</div>
)}
</>
) : (
<div className="text-center py-16 text-muted-foreground text-sm border border-dashed border-border/60">
Aucune contribution acceptée pour l'instant.
</div>
)}
<p className="text-xs text-muted-foreground/60 font-mono pt-8 mt-8 border-t border-border/30">
Contributions modérées selon le droit international des droits humains (DUDH · PIDCP · CEDH).{" "}
<a
href="/transparence"
className="underline text-primary"
>
Voir les critères de modération
</a>
.
</p>
</div>
</div>
);
}