Add sharing options and a printable flyer page to the website

Implement share and PDF export buttons on the home page, and create a new flyer page with a customizable QR code for printing and distribution.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a2b8df7d-660f-4020-961b-e37cb231d6a4
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/Z3YUti7
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
pironantoine
2026-04-04 10:24:23 +00:00
parent 213a67e612
commit 7e9eb3c360
7 changed files with 261 additions and 8 deletions
+95 -8
View File
@@ -27,7 +27,9 @@ import {
} from "@/components/ui/accordion";
import {
Loader2, PenTool, CheckCircle2, Info, AlertCircle, TrendingUp, Users, Scale,
Share2, Printer, Copy,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
const submitIdeaSchema = z.object({
content: z.string()
@@ -71,6 +73,7 @@ const VALEURS = [
export default function Home() {
const queryClient = useQueryClient();
const { toast } = useToast();
const [submitResult, setSubmitResult] = React.useState<{
success: boolean;
message: string;
@@ -84,6 +87,64 @@ export default function Home() {
query: { refetchInterval: 15000 },
});
const handleShare = () => {
if (!synthesis?.text) return;
const date = format(new Date(), "d MMMM yyyy 'à' HH:mm", { locale: fr });
const nb = synthesis.ideaCount ?? 0;
const shareText = `La Voix du Peuple — Synthèse citoyenne\nGénérée le ${date}\n\n${synthesis.text}\n\n(${nb} contribution${nb !== 1 ? "s" : ""} intégrée${nb !== 1 ? "s" : ""})\n\nhttps://lavoixdupeuple.fr`;
if (navigator.share) {
navigator.share({ title: "La Voix du Peuple — Synthèse", text: shareText });
} else {
navigator.clipboard.writeText(shareText).then(() => {
toast({ description: "Texte copié dans le presse-papier ✓" });
});
}
};
const handlePrint = () => {
if (!synthesis?.text) return;
const date = format(new Date(), "d MMMM yyyy 'à' HH:mm", { locale: fr });
const nb = synthesis.ideaCount ?? 0;
const safe = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const html = `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>La Voix du Peuple — Synthèse</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', 'Segoe UI', sans-serif; max-width: 680px; margin: 48px auto; color: #1a1a2e; line-height: 1.75; padding: 0 24px; }
.tricolor { display: flex; height: 4px; width: 80px; margin-bottom: 32px; border-radius: 2px; overflow: hidden; }
.tricolor span:nth-child(1) { flex: 1; background: #002395; }
.tricolor span:nth-child(2) { flex: 1; background: #EDEDED; }
.tricolor span:nth-child(3) { flex: 1; background: #ED2939; }
h1 { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 4px; color: #1a1a2e; }
.meta { font-size: 0.72rem; color: #888; font-family: monospace; margin-bottom: 36px; }
.text { font-size: 1rem; white-space: pre-line; color: #1a1a2e; }
.footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid #eee; font-size: 0.7rem; color: #bbb; font-family: monospace; display: flex; justify-content: space-between; }
@media print { body { margin: 32px auto; } }
</style>
</head>
<body>
<div class="tricolor"><span></span><span></span><span></span></div>
<h1>La Voix du Peuple — Synthèse citoyenne</h1>
<div class="meta">Générée le ${safe(date)} &nbsp;·&nbsp; ${nb} contribution${nb !== 1 ? "s" : ""} intégrée${nb !== 1 ? "s" : ""}</div>
<div class="text">${safe(synthesis.text)}</div>
<div class="footer">
<span>lavoixdupeuple.fr</span>
<span>Document généré automatiquement — modération selon DUDH / PIDCP / CEDH</span>
</div>
</body>
</html>`;
const w = window.open("", "_blank");
if (w) {
w.document.write(html);
w.document.close();
w.onload = () => w.print();
}
};
const form = useForm<SubmitIdeaValues>({
resolver: zodResolver(submitIdeaSchema),
defaultValues: { content: "", author: "" },
@@ -306,8 +367,8 @@ export default function Home() {
/>
{/* En-tête fixe */}
<div className="flex justify-between items-start px-6 md:px-10 py-5 border-b border-border/30 relative z-10 flex-shrink-0">
<div>
<div className="flex justify-between items-start px-6 md:px-10 py-5 border-b border-border/30 relative z-10 flex-shrink-0 gap-3">
<div className="min-w-0">
<h2 className="text-xs font-mono font-bold uppercase tracking-widest text-primary flex items-center gap-2">
<Users className="h-3.5 w-3.5" /> Synthèse des contributions
</h2>
@@ -315,12 +376,38 @@ export default function Home() {
Mise à jour à chaque nouvelle contribution
</p>
</div>
{stats && (
<div className="text-xs font-mono text-right" data-testid="text-stats">
<span className="text-muted-foreground">Contributions intégrées </span>
<span className="font-bold text-primary">{stats.accepted}</span>
</div>
)}
<div className="flex items-center gap-2 flex-shrink-0">
{stats && (
<div className="text-xs font-mono text-right mr-2" data-testid="text-stats">
<span className="text-muted-foreground">Intégrées </span>
<span className="font-bold text-primary">{stats.accepted}</span>
</div>
)}
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs gap-1.5 text-muted-foreground hover:text-foreground"
onClick={handleShare}
disabled={!synthesis?.text}
title="Partager ou copier la synthèse"
>
{navigator.share ? (
<><Share2 className="h-3.5 w-3.5" /> Partager</>
) : (
<><Copy className="h-3.5 w-3.5" /> Copier</>
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs gap-1.5 text-muted-foreground hover:text-foreground"
onClick={handlePrint}
disabled={!synthesis?.text}
title="Exporter en PDF / imprimer"
>
<Printer className="h-3.5 w-3.5" /> PDF
</Button>
</div>
</div>
{/* Texte défilable */}