bae1335dd5
- 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>
193 lines
7.0 KiB
TypeScript
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>•</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>
|
|
);
|
|
}
|