Add mobile overflow nav, law check feature, and favicon

Nav: "Fonctionnement" and "Flyer QR" restored on mobile via a ···
overflow dropdown (click-outside closes); other links always visible

Law check: new POST /api/check-law endpoint (5/min, 20/h) calls
mistral-small with a new LAW_CHECK_PROMPT to detect if a proposal is
already covered by French or EU law. Informational only, non-blocking.
Frontend: "Vérifier le cadre légal existant" button below textarea in
home.tsx; result displayed with Scale icon; resets on content change.

Favicon: replaced placeholder red square with a petrol rounded square
containing a serif "V" (for Voix) in warm cream — matches brand palette

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 17:57:29 +02:00
parent 5c5152a387
commit 2ef14ac765
6 changed files with 179 additions and 7 deletions
+14 -2
View File
@@ -1,3 +1,15 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="180" height="180" rx="36" fill="#FF3C00"/>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Fond arrondi couleur primaire petrol -->
<rect width="32" height="32" rx="7" fill="#29676F"/>
<!-- Lettre V stylisée centrée, police serif grasse -->
<text
x="16" y="24"
font-family="Georgia, 'Times New Roman', serif"
font-size="22"
font-weight="700"
fill="#F9F7F1"
text-anchor="middle"
dominant-baseline="auto"
letter-spacing="-1"
>V</text>
</svg>

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 494 B

+48 -2
View File
@@ -30,17 +30,63 @@ const queryClient = new QueryClient({
});
function Navbar() {
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!mobileMenuOpen) return;
const handler = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMobileMenuOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [mobileMenuOpen]);
return (
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto max-w-7xl flex h-16 items-center px-4 md:px-8">
<Link href="/" className="flex items-center gap-2 mr-6 font-serif text-xl font-bold tracking-tight text-primary" data-testid="nav-home-link">
<Link href="/" className="flex items-center gap-2 mr-4 sm:mr-6 font-serif text-lg sm:text-xl font-bold tracking-tight text-primary flex-shrink-0" data-testid="nav-home-link">
La Voix du Peuple
</Link>
<nav className="flex items-center gap-4 text-sm font-medium" aria-label="Navigation principale">
<nav className="flex items-center gap-3 sm:gap-5 text-sm font-medium" aria-label="Navigation principale">
<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>
<Link href="/transparence" className="hidden sm:inline transition-colors hover:text-foreground/80 text-foreground/60" data-testid="nav-transparence-link">Fonctionnement</Link>
<Link href="/flyer" className="hidden sm:inline transition-colors hover:text-foreground/80 text-foreground/60" data-testid="nav-flyer-link">Flyer QR</Link>
{/* Menu débordement mobile */}
<div className="relative sm:hidden" ref={menuRef}>
<button
onClick={() => setMobileMenuOpen(v => !v)}
aria-label="Plus de pages"
aria-expanded={mobileMenuOpen}
className="text-foreground/60 hover:text-foreground/80 transition-colors px-1 text-base leading-none"
>
···
</button>
{mobileMenuOpen && (
<div className="absolute left-0 top-7 bg-background border border-border/60 rounded-md shadow-md py-1 min-w-36 z-50">
<Link
href="/transparence"
className="block px-4 py-2 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 transition-colors"
onClick={() => setMobileMenuOpen(false)}
data-testid="nav-transparence-link-mobile"
>
Fonctionnement
</Link>
<Link
href="/flyer"
className="block px-4 py-2 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 transition-colors"
onClick={() => setMobileMenuOpen(false)}
data-testid="nav-flyer-link-mobile"
>
Flyer QR
</Link>
</div>
)}
</div>
</nav>
<div className="ml-auto flex items-center gap-2">
<AccessibilityPanel />
+52 -1
View File
@@ -35,7 +35,7 @@ import {
} from "@/components/ui/accordion";
import {
Loader2, PenTool, CheckCircle2, Info, AlertCircle, TrendingUp, Users, Scale,
Share2, Printer, Copy, Flag,
Share2, Printer, Copy, Flag, BookOpen,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
@@ -143,6 +143,8 @@ export default function Home() {
}, [countdown]);
const [mobileTab, setMobileTab] = React.useState<"proposer" | "synthese">("proposer");
const [lawCheckResult, setLawCheckResult] = React.useState<string | null>(null);
const [lawCheckLoading, setLawCheckLoading] = React.useState(false);
// Soumission effective après confirmation du consentement (ou si déjà consenti)
const doActualSubmit = (data: SubmitIdeaValues) => {
@@ -206,6 +208,26 @@ export default function Home() {
}
};
const checkLaw = async () => {
const content = form.getValues("content")?.trim();
if (!content || content.length < 10) return;
setLawCheckLoading(true);
setLawCheckResult(null);
try {
const res = await fetch(`${API_BASE}/api/check-law`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
const data = await res.json();
setLawCheckResult(data.note || null);
} catch {
setLawCheckResult(null);
} finally {
setLawCheckLoading(false);
}
};
const handleShare = () => {
if (!synthesis?.text) return;
const date = format(new Date(), "d MMMM yyyy 'à' HH:mm", { locale: fr });
@@ -376,6 +398,10 @@ export default function Home() {
className="min-h-[90px] sm: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}
onChange={(e) => {
field.onChange(e);
if (lawCheckResult) setLawCheckResult(null);
}}
/>
</FormControl>
<FormMessage />
@@ -383,6 +409,31 @@ export default function Home() {
)}
/>
{/* Vérification cadre légal existant */}
<div className="flex items-start gap-2 flex-wrap">
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 px-2 text-xs gap-1.5 text-muted-foreground hover:text-foreground"
disabled={lawCheckLoading || (form.getValues("content")?.trim().length ?? 0) < 10}
onClick={checkLaw}
title="Vérifier si cette proposition est déjà inscrite dans la loi française ou européenne"
>
{lawCheckLoading
? <Loader2 className="h-3 w-3 animate-spin" />
: <BookOpen className="h-3 w-3" />}
{lawCheckLoading ? "Vérification…" : "Vérifier le cadre légal existant"}
</Button>
</div>
{lawCheckResult && (
<div className="text-xs border border-primary/20 bg-primary/5 rounded-md px-3 py-2.5 text-foreground/80 leading-relaxed flex gap-2">
<Scale className="h-3.5 w-3.5 text-primary flex-shrink-0 mt-0.5" />
<span>{lawCheckResult}</span>
</div>
)}
<div className="flex items-start gap-4">
<FormField
control={form.control}