Update website to display all content in French and add a values disclaimer
Translate UI elements to French, replace rejected voice notifications with a disclaimer, and add sourced legal values. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fcd1a349-b688-425c-9bb4-acdbfc2e808e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/zrclMhx Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -23,8 +23,8 @@ function Navbar() {
|
||||
La Voix du Peuple
|
||||
</Link>
|
||||
<nav className="flex items-center gap-6 text-sm font-medium">
|
||||
<Link href="/" className="transition-colors hover:text-foreground/80 text-foreground/60" data-testid="nav-manifesto-link">Manifesto</Link>
|
||||
<Link href="/about" className="transition-colors hover:text-foreground/80 text-foreground/60" data-testid="nav-about-link">About</Link>
|
||||
<Link href="/" className="transition-colors hover:text-foreground/80 text-foreground/60" data-testid="nav-manifesto-link">Manifeste</Link>
|
||||
<Link href="/about" className="transition-colors hover:text-foreground/80 text-foreground/60" data-testid="nav-about-link">À propos</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,22 +1,104 @@
|
||||
import React from "react";
|
||||
import { Link } from "wouter";
|
||||
import { ArrowLeft, Shield, Brain, BookOpen } from "lucide-react";
|
||||
import { ArrowLeft, Shield, Brain, BookOpen, Scale, ExternalLink } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const TEXTES_SOURCES = [
|
||||
{
|
||||
sigle: "DUDH",
|
||||
titre: "Déclaration universelle des droits de l'homme",
|
||||
org: "Organisation des Nations Unies",
|
||||
annee: "1948",
|
||||
url: "https://www.un.org/fr/about-us/universal-declaration-of-human-rights",
|
||||
articles: [
|
||||
{ num: "Art. 1", texte: "Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité." },
|
||||
{ num: "Art. 2", texte: "Chacun peut se prévaloir de tous les droits et de toutes les libertés proclamés dans la présente Déclaration, sans distinction aucune, notamment de race, de couleur, de sexe, de langue, de religion, d'opinion politique ou de toute autre opinion, d'origine nationale ou sociale, de fortune, de naissance ou de toute autre situation." },
|
||||
{ num: "Art. 19", texte: "Tout individu a droit à la liberté d'opinion et d'expression, ce qui implique le droit de ne pas être inquiété pour ses opinions et celui de chercher, de recevoir et de répandre, sans considérations de frontières, les informations et les idées par quelque moyen d'expression que ce soit." },
|
||||
{ num: "Art. 20", texte: "Toute propagande en faveur de la guerre est interdite par la loi. Tout appel à la haine nationale, raciale ou religieuse qui constitue une incitation à la discrimination, à l'hostilité ou à la violence est interdit par la loi." },
|
||||
],
|
||||
},
|
||||
{
|
||||
sigle: "PIDCP",
|
||||
titre: "Pacte international relatif aux droits civils et politiques",
|
||||
org: "Organisation des Nations Unies",
|
||||
annee: "1966",
|
||||
url: "https://www.ohchr.org/fr/instruments-mechanisms/instruments/international-covenant-civil-and-political-rights",
|
||||
articles: [
|
||||
{ num: "Art. 20", texte: "Tout appel à la haine nationale, raciale ou religieuse qui constitue une incitation à la discrimination, à l'hostilité ou à la violence est interdit par la loi." },
|
||||
{ num: "Art. 25", texte: "Tout citoyen a le droit et la possibilité, sans restrictions déraisonnables, de prendre part à la direction des affaires publiques, soit directement, soit par l'intermédiaire de représentants librement choisis." },
|
||||
{ num: "Art. 26", texte: "Toutes les personnes sont égales devant la loi et ont droit sans discrimination à une égale protection de la loi." },
|
||||
],
|
||||
},
|
||||
{
|
||||
sigle: "CEDH",
|
||||
titre: "Convention européenne des droits de l'homme",
|
||||
org: "Conseil de l'Europe",
|
||||
annee: "1950",
|
||||
url: "https://www.echr.coe.int/european-convention-on-human-rights",
|
||||
articles: [
|
||||
{ num: "Art. 10", texte: "Toute personne a droit à la liberté d'expression. Ce droit comprend la liberté d'opinion et la liberté de recevoir ou de communiquer des informations ou des idées sans qu'il puisse y avoir ingérence d'autorités publiques et sans considération de frontière." },
|
||||
{ num: "Art. 17", texte: "Aucune des dispositions de la présente Convention ne peut être interprétée comme impliquant pour un État, un groupement ou un individu, un droit quelconque de se livrer à une activité ou d'accomplir un acte visant à la destruction des droits ou libertés reconnus dans la présente Convention." },
|
||||
],
|
||||
},
|
||||
{
|
||||
sigle: "Charte UE",
|
||||
titre: "Charte des droits fondamentaux de l'Union européenne",
|
||||
org: "Union européenne",
|
||||
annee: "2000 / entrée en vigueur 2009",
|
||||
url: "https://www.europarl.europa.eu/charter/pdf/text_fr.pdf",
|
||||
articles: [
|
||||
{ num: "Art. 1", texte: "La dignité humaine est inviolable. Elle doit être respectée et protégée." },
|
||||
{ num: "Art. 21", texte: "Est interdite toute discrimination fondée notamment sur le sexe, la race, la couleur, les origines ethniques ou sociales, les caractéristiques génétiques, la langue, la religion ou les convictions, les opinions politiques ou toute autre opinion." },
|
||||
],
|
||||
},
|
||||
{
|
||||
sigle: "Convention génocide",
|
||||
titre: "Convention pour la prévention et la répression du crime de génocide",
|
||||
org: "Organisation des Nations Unies",
|
||||
annee: "1948",
|
||||
url: "https://www.un.org/fr/genocideprevention/genocide-convention.shtml",
|
||||
articles: [
|
||||
{ num: "Art. III (c)", texte: "L'incitation directe et publique à commettre le génocide est punie." },
|
||||
],
|
||||
},
|
||||
{
|
||||
sigle: "Statut de Rome",
|
||||
titre: "Statut de Rome de la Cour pénale internationale",
|
||||
org: "Cour pénale internationale",
|
||||
annee: "1998",
|
||||
url: "https://www.icc-cpi.int/sites/default/files/RS-Fra.pdf",
|
||||
articles: [
|
||||
{ num: "Art. 7", texte: "On entend par « crime contre l'humanité » l'un quelconque des actes ci-après lorsqu'il est commis dans le cadre d'une attaque généralisée ou systématique lancée contre toute population civile et en connaissance de cette attaque : persécution de tout groupe ou de toute collectivité identifiable pour des motifs d'ordre politique, racial, national, ethnique, culturel ou religieux." },
|
||||
],
|
||||
},
|
||||
{
|
||||
sigle: "CERD",
|
||||
titre: "Convention internationale sur l'élimination de toutes les formes de discrimination raciale",
|
||||
org: "Organisation des Nations Unies",
|
||||
annee: "1965",
|
||||
url: "https://www.ohchr.org/fr/instruments-mechanisms/instruments/international-convention-elimination-all-forms-racial",
|
||||
articles: [
|
||||
{ num: "Art. 4", texte: "Les États parties condamnent toute propagande et toutes organisations qui s'inspirent d'idées ou de théories fondées sur la supériorité d'une race ou d'un groupe de personnes d'une certaine couleur ou d'une certaine origine ethnique, ou qui prétendent justifier ou encourager toute forme de haine et de discrimination raciales." },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-4rem)] bg-background flex justify-center py-12 px-4 md:px-8">
|
||||
<div className="max-w-3xl w-full">
|
||||
<Link href="/">
|
||||
<Button variant="ghost" size="sm" className="mb-8 font-mono tracking-widest uppercase text-xs">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> Return to the Agora
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> Retour à l'agora
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<header className="mb-12 space-y-4">
|
||||
<h1 className="text-4xl md:text-5xl font-serif font-bold text-primary">About La Voix du Peuple</h1>
|
||||
<h1 className="text-4xl md:text-5xl font-serif font-bold text-primary">
|
||||
À propos de La Voix du Peuple
|
||||
</h1>
|
||||
<p className="text-xl font-serif text-muted-foreground leading-relaxed">
|
||||
A digital town square where individual voices forge a collective democratic manifesto, protected by artificial intelligence.
|
||||
Une place publique numérique où les voix individuelles forment un manifeste démocratique collectif, guidé par le droit international des droits humains.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -24,44 +106,95 @@ export default function About() {
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-xl font-bold font-serif border-b border-border/50 pb-2">
|
||||
<BookOpen className="h-6 w-6 text-primary" />
|
||||
<h2>The Concept</h2>
|
||||
<h2>Le concept</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg">
|
||||
Democracy requires both free expression and a shared foundation of values. "La Voix du Peuple" (The Voice of the People) provides a platform where citizens can submit their ideas for the betterment of society. These distinct, fragile voices are woven together in real-time to create a living, breathing synthesis—a modern digital pamphlet reflecting our collective aspirations.
|
||||
La démocratie exige à la fois la liberté d'expression et un socle commun de valeurs. Cette plateforme offre aux citoyens un espace pour soumettre leurs idées en faveur d'une société meilleure. Ces voix distinctes sont tissées en temps réel pour former une synthèse vivante — un pamphlet numérique moderne qui reflète nos aspirations collectives.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-xl font-bold font-serif border-b border-border/50 pb-2">
|
||||
<Shield className="h-6 w-6 text-primary" />
|
||||
<h2>The Democratic Filter</h2>
|
||||
<h2>Le filtre démocratique</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg mb-4">
|
||||
Not all speech serves the public good. To protect the integrity of the agora, an AI agent acts as a civic guardian. Every submitted idea is evaluated against core democratic principles before it can enter the collective synthesis.
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg">
|
||||
Toute prise de parole ne contribue pas nécessairement au bien commun. Pour préserver l'intégrité de l'agora, un agent d'intelligence artificielle agit comme gardien civique. Chaque idée soumise est évaluée à la lumière des droits fondamentaux reconnus par le droit international avant de pouvoir rejoindre la synthèse collective.
|
||||
</p>
|
||||
<div className="bg-muted/30 p-6 rounded-none border-l-4 border-primary">
|
||||
<h3 className="font-bold font-mono uppercase tracking-widest text-sm mb-3">Protected Values</h3>
|
||||
<h3 className="font-bold font-mono uppercase tracking-widest text-sm mb-3">
|
||||
Valeurs protégées
|
||||
</h3>
|
||||
<ul className="space-y-2 font-serif list-disc pl-5">
|
||||
<li>Respect for human rights and dignity</li>
|
||||
<li>Equality and anti-discrimination</li>
|
||||
<li>Constructive civic discourse (no hate speech or violence)</li>
|
||||
<li>Commitment to democratic processes</li>
|
||||
<li>La dignité humaine universelle et inviolable</li>
|
||||
<li>L'égalité et la non-discrimination</li>
|
||||
<li>La liberté d'expression dans le respect des droits d'autrui</li>
|
||||
<li>Le refus de la haine, de la violence et des appels à la persécution</li>
|
||||
<li>L'attachement aux processus démocratiques et pacifiques</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg mt-4">
|
||||
If an idea violates these principles, it is rejected with a clear explanation. This transparency ensures the filter remains accountable to the people it serves.
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg">
|
||||
Lorsqu'une contribution ne peut être retenue, l'auteur en est informé discrètement, sans jugement de sa personne — seulement de la conformité du contenu aux valeurs fondamentales de cet espace commun.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-xl font-bold font-serif border-b border-border/50 pb-2">
|
||||
<Brain className="h-6 w-6 text-primary" />
|
||||
<h2>The Synthesis</h2>
|
||||
<h2>La synthèse</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg">
|
||||
Once accepted, an idea doesn't just sit in a list. A second AI agent constantly reads the stream of accepted ideas and weaves them into a single, cohesive narrative. This text—updated every 15 seconds—represents the prevailing mood, concerns, and hopes of the populace. It is the voice of the people, distilled.
|
||||
Une idée acceptée ne reste pas isolée. Un second agent lit en continu le flux des idées validées et les tisse en un texte cohérent, éloquent, rédigé à la première personne du pluriel — « Nous, le peuple ». Ce texte, actualisé toutes les 15 secondes, représente l'humeur collective, les préoccupations et les espoirs des citoyens qui ont pris la parole.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<div className="flex items-center gap-3 text-xl font-bold font-serif border-b border-border/50 pb-2">
|
||||
<Scale className="h-6 w-6 text-primary" />
|
||||
<h2>Fondements juridiques</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-foreground/90 font-serif text-lg">
|
||||
Le filtre s'appuie sur les textes fondateurs du droit international des droits humains. Voici les instruments qui guident chaque décision d'intégration ou de non-intégration d'une contribution.
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
{TEXTES_SOURCES.map((source) => (
|
||||
<div key={source.sigle} className="border border-border/40 p-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<span className="font-mono text-xs font-bold text-primary bg-primary/10 px-2 py-0.5 rounded-sm">
|
||||
{source.sigle}
|
||||
</span>
|
||||
<h3 className="font-serif font-bold text-lg mt-2">{source.titre}</h3>
|
||||
<p className="text-xs font-mono text-muted-foreground mt-0.5">
|
||||
{source.org} · {source.annee}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 text-muted-foreground hover:text-primary transition-colors"
|
||||
title="Consulter le texte officiel"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{source.articles.map((art) => (
|
||||
<div key={art.num} className="border-l-2 border-primary/30 pl-4">
|
||||
<p className="text-xs font-mono font-bold text-primary/80 mb-1">{art.num}</p>
|
||||
<p className="font-serif text-sm text-foreground/80 leading-relaxed italic">
|
||||
« {art.texte} »
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
useSubmitIdea,
|
||||
@@ -10,45 +11,82 @@ import {
|
||||
useGetIdeaStats,
|
||||
useGetSynthesis,
|
||||
getListIdeasQueryKey,
|
||||
getGetIdeaStatsQueryKey
|
||||
getGetIdeaStatsQueryKey,
|
||||
} from "@workspace/api-client-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Loader2, PenTool, CheckCircle2, XCircle, AlertCircle, TrendingUp, Users } from "lucide-react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import {
|
||||
Loader2, PenTool, CheckCircle2, Info, AlertCircle, TrendingUp, Users, Scale,
|
||||
} from "lucide-react";
|
||||
|
||||
const submitIdeaSchema = z.object({
|
||||
content: z.string()
|
||||
.min(10, "Your idea must be at least 10 characters long to carry weight.")
|
||||
.max(1000, "Brevity is the soul of wit. Keep it under 1000 characters."),
|
||||
.min(10, "Votre idée doit comporter au moins 10 caractères pour avoir du poids.")
|
||||
.max(1000, "La concision est une vertu. Restez sous 1 000 caractères."),
|
||||
author: z.string().max(100).optional(),
|
||||
});
|
||||
|
||||
type SubmitIdeaValues = z.infer<typeof submitIdeaSchema>;
|
||||
|
||||
const VALEURS = [
|
||||
{
|
||||
source: "DUDH — Art. 1 (ONU, 1948)",
|
||||
texte: "Tous les êtres humains naissent libres et égaux en dignité et en droits.",
|
||||
},
|
||||
{
|
||||
source: "DUDH — Art. 19",
|
||||
texte: "Tout individu a droit à la liberté d'opinion et d'expression.",
|
||||
},
|
||||
{
|
||||
source: "DUDH — Art. 20",
|
||||
texte: "Tout appel à la haine nationale, raciale ou religieuse constituant une incitation à la discrimination, à l'hostilité ou à la violence est interdit par la loi.",
|
||||
},
|
||||
{
|
||||
source: "PIDCP — Art. 20 (ONU, 1966)",
|
||||
texte: "Tout appel à la haine nationale, raciale ou religieuse qui constitue une incitation à la discrimination, à l'hostilité ou à la violence est interdit par la loi.",
|
||||
},
|
||||
{
|
||||
source: "CEDH — Art. 10 (Conseil de l'Europe, 1950)",
|
||||
texte: "Toute personne a droit à la liberté d'expression, sous réserve des restrictions nécessaires à la protection des droits d'autrui.",
|
||||
},
|
||||
{
|
||||
source: "CEDH — Art. 17",
|
||||
texte: "Aucune disposition de la Convention ne peut être interprétée comme impliquant le droit de se livrer à une activité visant à la destruction des droits reconnus.",
|
||||
},
|
||||
{
|
||||
source: "Charte des droits fondamentaux de l'UE — Art. 1 (2000)",
|
||||
texte: "La dignité humaine est inviolable. Elle doit être respectée et protégée.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const queryClient = useQueryClient();
|
||||
const [submitResult, setSubmitResult] = React.useState<{ success: boolean; message: string; reason?: string } | null>(null);
|
||||
const [submitResult, setSubmitResult] = React.useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
reason?: string;
|
||||
} | null>(null);
|
||||
|
||||
const submitIdea = useSubmitIdea();
|
||||
const { data: ideas, isLoading: isLoadingIdeas } = useListIdeas();
|
||||
const { data: stats } = useGetIdeaStats();
|
||||
const { data: synthesis, isLoading: isLoadingSynthesis } = useGetSynthesis({
|
||||
query: {
|
||||
refetchInterval: 15000,
|
||||
}
|
||||
query: { refetchInterval: 15000 },
|
||||
});
|
||||
|
||||
const form = useForm<SubmitIdeaValues>({
|
||||
resolver: zodResolver(submitIdeaSchema),
|
||||
defaultValues: {
|
||||
content: "",
|
||||
author: "",
|
||||
},
|
||||
defaultValues: { content: "", author: "" },
|
||||
});
|
||||
|
||||
const onSubmit = (data: SubmitIdeaValues) => {
|
||||
@@ -58,39 +96,39 @@ export default function Home() {
|
||||
if (result.accepted) {
|
||||
setSubmitResult({
|
||||
success: true,
|
||||
message: "Your voice has been added to the collective manifesto."
|
||||
message: "Votre voix a été intégrée au manifeste collectif.",
|
||||
});
|
||||
form.reset();
|
||||
} else {
|
||||
setSubmitResult({
|
||||
success: false,
|
||||
message: "Your submission was rejected by the democratic filter.",
|
||||
reason: result.reason
|
||||
message: "Cette contribution n'a pas pu être intégrée au manifeste car elle n'est pas en accord avec les valeurs fondamentales qui guident cet espace commun.",
|
||||
reason: result.reason ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// Invalidate queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: getListIdeasQueryKey() });
|
||||
queryClient.invalidateQueries({ queryKey: getGetIdeaStatsQueryKey() });
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: () => {
|
||||
setSubmitResult({
|
||||
success: false,
|
||||
message: "An error occurred while submitting your idea. Please try again."
|
||||
message: "Une erreur est survenue lors de l'envoi. Veuillez réessayer.",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 grid md:grid-cols-2 lg:grid-cols-[1fr_1.2fr] h-[calc(100vh-4rem)]">
|
||||
{/* Left Column: Input & Feed */}
|
||||
{/* Colonne gauche : formulaire + fil des idées */}
|
||||
<div className="flex flex-col border-r border-border/40 bg-card">
|
||||
<div className="p-6 md:p-8 flex flex-col gap-6 flex-shrink-0 border-b border-border/40">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-serif font-bold text-primary tracking-tight">Speak to the Republic</h1>
|
||||
<h1 className="text-3xl font-serif font-bold text-primary tracking-tight">
|
||||
Parlez à la République
|
||||
</h1>
|
||||
<p className="text-muted-foreground font-mono text-sm uppercase tracking-wider">
|
||||
Your voice matters. Submit your vision for the future.
|
||||
Votre voix compte. Partagez votre vision pour l'avenir.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -101,10 +139,10 @@ export default function Home() {
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="sr-only">Your Idea</FormLabel>
|
||||
<FormLabel className="sr-only">Votre idée</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="What change do we need? Speak plainly."
|
||||
placeholder="Quel changement souhaitez-vous ? Exprimez-vous librement."
|
||||
className="min-h-[120px] resize-none font-serif text-lg bg-background border-primary/20 focus-visible:ring-primary placeholder:text-muted-foreground/50"
|
||||
data-testid="input-idea-content"
|
||||
{...field}
|
||||
@@ -121,10 +159,10 @@ export default function Home() {
|
||||
name="author"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel className="sr-only">Pseudonym (Optional)</FormLabel>
|
||||
<FormLabel className="sr-only">Pseudonyme (optionnel)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Pseudonym (optional)"
|
||||
placeholder="Pseudonyme (optionnel)"
|
||||
className="bg-background border-primary/20 focus-visible:ring-primary font-mono text-sm"
|
||||
data-testid="input-idea-author"
|
||||
{...field}
|
||||
@@ -141,9 +179,9 @@ export default function Home() {
|
||||
data-testid="button-submit-idea"
|
||||
>
|
||||
{submitIdea.isPending ? (
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Publishing...</>
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Envoi…</>
|
||||
) : (
|
||||
<><PenTool className="mr-2 h-4 w-4" /> Proclaim</>
|
||||
<><PenTool className="mr-2 h-4 w-4" /> Proclamer</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -151,26 +189,65 @@ export default function Home() {
|
||||
</Form>
|
||||
|
||||
{submitResult && (
|
||||
<Alert variant={submitResult.success ? "default" : "destructive"} className="mt-2" data-testid={`alert-submit-${submitResult.success ? 'success' : 'error'}`}>
|
||||
{submitResult.success ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
|
||||
<AlertTitle>{submitResult.success ? "Proclamation Accepted" : "Proclamation Rejected"}</AlertTitle>
|
||||
<Alert
|
||||
variant={submitResult.success ? "default" : "default"}
|
||||
className={`mt-2 ${submitResult.success ? "" : "border-amber-300 bg-amber-50 text-amber-900"}`}
|
||||
data-testid={`alert-submit-${submitResult.success ? "success" : "info"}`}
|
||||
>
|
||||
{submitResult.success
|
||||
? <CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
: <Info className="h-4 w-4 text-amber-600" />
|
||||
}
|
||||
<AlertTitle>
|
||||
{submitResult.success ? "Proclamation enregistrée" : "Contribution non retenue"}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{submitResult.message}
|
||||
{submitResult.reason && (
|
||||
<div className="mt-2 text-sm italic border-l-2 pl-2 py-1 border-current opacity-90">
|
||||
Reason: {submitResult.reason}
|
||||
{submitResult.reason && !submitResult.success && (
|
||||
<div className="mt-2 text-sm italic border-l-2 pl-2 py-1 border-amber-400 opacity-90">
|
||||
{submitResult.reason}
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Encart valeurs */}
|
||||
<Accordion type="single" collapsible className="border border-border/40 rounded-sm px-3">
|
||||
<AccordionItem value="valeurs" className="border-none">
|
||||
<AccordionTrigger className="text-xs font-mono uppercase tracking-widest text-muted-foreground hover:no-underline py-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<Scale className="h-3 w-3" /> Nos valeurs & fondements juridiques
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<p className="text-xs text-muted-foreground font-serif mb-4 leading-relaxed">
|
||||
Cet espace est régi par les grands textes fondateurs du droit international
|
||||
des droits humains. Toute contribution est évaluée à leur lumière.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{VALEURS.map((v) => (
|
||||
<div key={v.source} className="border-l-2 border-primary/30 pl-3">
|
||||
<p className="text-xs font-mono font-semibold text-primary/80 mb-0.5">{v.source}</p>
|
||||
<p className="text-xs font-serif text-foreground/80 leading-relaxed italic">« {v.texte} »</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-mono mt-4">
|
||||
Sources : DUDH (ONU 1948) · PIDCP (ONU 1966) · CEDH (Conseil de l'Europe 1950) ·
|
||||
Charte des droits fondamentaux de l'UE (2000) · Statut de Rome / CPI (1998) ·
|
||||
CERD (ONU 1965)
|
||||
</p>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
{/* Feed of recent accepted ideas */}
|
||||
{/* Fil des proclamations acceptées */}
|
||||
<ScrollArea className="flex-1 bg-muted/30">
|
||||
<div className="p-6 md:p-8">
|
||||
<h2 className="text-xs font-mono font-bold uppercase tracking-widest text-muted-foreground mb-6 flex items-center gap-2">
|
||||
<TrendingUp className="h-3 w-3" /> Recent Proclamations
|
||||
<TrendingUp className="h-3 w-3" /> Proclamations récentes
|
||||
</h2>
|
||||
|
||||
<div className="space-y-8">
|
||||
@@ -180,20 +257,28 @@ export default function Home() {
|
||||
</div>
|
||||
) : ideas && ideas.length > 0 ? (
|
||||
ideas.slice(0, 20).map((idea) => (
|
||||
<div key={idea.id} className="group relative pl-4 border-l border-border/60 hover:border-primary/50 transition-colors" data-testid={`card-idea-${idea.id}`}>
|
||||
<div
|
||||
key={idea.id}
|
||||
className="group relative pl-4 border-l border-border/60 hover:border-primary/50 transition-colors"
|
||||
data-testid={`card-idea-${idea.id}`}
|
||||
>
|
||||
<p className="text-foreground/90 font-serif leading-relaxed">
|
||||
{idea.content}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center gap-3 text-xs font-mono text-muted-foreground">
|
||||
<span className="font-semibold text-primary/70">{idea.author || "Anonymous Citizen"}</span>
|
||||
<span className="font-semibold text-primary/70">
|
||||
{idea.author || "Citoyen anonyme"}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{format(new Date(idea.createdAt), "MMM d, h:mm a")}</span>
|
||||
<span>
|
||||
{format(new Date(idea.createdAt), "d MMM, HH:mm", { locale: fr })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center p-8 text-muted-foreground font-mono text-sm border border-dashed border-border/60">
|
||||
No proclamations recorded yet. Be the first to speak.
|
||||
Aucune proclamation enregistrée. Soyez le premier à prendre la parole.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -201,11 +286,14 @@ export default function Home() {
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Synthesis */}
|
||||
{/* Colonne droite : synthèse vivante */}
|
||||
<div className="flex flex-col bg-[#F9F7F1] relative overflow-hidden">
|
||||
{/* Decorative noise/texture overlay could go here */}
|
||||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none mix-blend-multiply"
|
||||
style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22/%3E%3C/svg%3E")' }}></div>
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03] pointer-events-none mix-blend-multiply"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="p-6 md:p-12 flex-1 flex flex-col relative z-10 overflow-y-auto">
|
||||
<div className="flex justify-between items-start mb-12">
|
||||
@@ -214,20 +302,16 @@ export default function Home() {
|
||||
<Users className="h-4 w-4" /> La Voix du Peuple
|
||||
</h2>
|
||||
<p className="text-xs font-mono text-muted-foreground mt-1">
|
||||
The living synthesis of democratic thought
|
||||
La synthèse vivante de la pensée démocratique
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{stats && (
|
||||
<div className="flex gap-4 text-xs font-mono text-right" data-testid="text-stats">
|
||||
<div className="text-xs font-mono text-right" data-testid="text-stats">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-muted-foreground">Voices Joined</span>
|
||||
<span className="text-muted-foreground">Voix rejointes</span>
|
||||
<span className="font-bold text-primary">{stats.accepted}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-muted-foreground">Voices Rejected</span>
|
||||
<span className="font-bold text-destructive">{stats.rejected}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,7 +320,9 @@ export default function Home() {
|
||||
{isLoadingSynthesis ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-muted-foreground">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="font-mono text-sm uppercase tracking-widest">Listening to the people...</span>
|
||||
<span className="font-mono text-sm uppercase tracking-widest">
|
||||
À l'écoute du peuple…
|
||||
</span>
|
||||
</div>
|
||||
) : synthesis ? (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-1000 ease-out space-y-8">
|
||||
@@ -245,21 +331,28 @@ export default function Home() {
|
||||
data-testid="text-synthesis-content"
|
||||
>
|
||||
{synthesis.text ? (
|
||||
<div dangerouslySetExpandedAutocorrect={undefined}>{synthesis.text}</div>
|
||||
<div>{synthesis.text}</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground italic text-center text-xl">The pages of our manifesto remain empty. Speak, and it shall be written.</p>
|
||||
<p className="text-muted-foreground italic text-center text-xl">
|
||||
Les pages de notre manifeste sont encore vierges. Prenez la parole.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{synthesis.updatedAt && (
|
||||
<div className="pt-8 border-t border-border/40 mt-12 flex justify-between items-center text-xs font-mono text-muted-foreground" data-testid="text-synthesis-meta">
|
||||
<span>Synthesized from {synthesis.ideaCount} democratic ideas</span>
|
||||
<div
|
||||
className="pt-8 border-t border-border/40 mt-12 flex justify-between items-center text-xs font-mono text-muted-foreground"
|
||||
data-testid="text-synthesis-meta"
|
||||
>
|
||||
<span>
|
||||
Synthèse de {synthesis.ideaCount} idée{synthesis.ideaCount !== 1 ? "s" : ""} citoyenne{synthesis.ideaCount !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
Live • {format(new Date(synthesis.updatedAt), "HH:mm:ss")}
|
||||
En direct • {format(new Date(synthesis.updatedAt), "HH:mm:ss")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -267,7 +360,9 @@ export default function Home() {
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-muted-foreground">
|
||||
<AlertCircle className="h-8 w-8 text-destructive" />
|
||||
<span className="font-mono text-sm uppercase tracking-widest">Failed to retrieve the manifesto</span>
|
||||
<span className="font-mono text-sm uppercase tracking-widest">
|
||||
Impossible de récupérer le manifeste
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,10 @@ export default function NotFound() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex mb-4 gap-2">
|
||||
<AlertCircle className="h-8 w-8 text-red-500" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">404 Page Not Found</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900">404 — Page introuvable</h1>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm text-gray-600">
|
||||
Did you forget to add the page to the router?
|
||||
Cette page n'existe pas.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user