spb/vrai-prix Public
Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 96.7%
CSS 3.1%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Navigation d'en-tête. Le menu mobile utilise le motif checkbox + peer-checked :4 * il s'ouvre et se ferme en PUR CSS — fonctionne même sans JavaScript (cache5 * périmé, hydratation cassée, JS bloqué). Le JS n'est qu'une amélioration6 * (fermeture au changement de page, verrou de défilement).7 */8"use client";9import { useEffect, useRef } from "react";10import Link from "next/link";11import { usePathname } from "next/navigation";12import { useLang } from "./LangContext";1314export default function HeaderNav() {15 const { lang, setLang } = useLang();16 const path = usePathname();17 const fr = lang === "fr";18 const box = useRef<HTMLInputElement>(null);1920 // améliorations JS (facultatives) : fermer à la navigation, verrouiller le scroll21 useEffect(() => {22 if (box.current) box.current.checked = false;23 document.body.style.overflow = "";24 }, [path]);25 const onToggle = () => {26 document.body.style.overflow = box.current?.checked ? "hidden" : "";27 };2829 const items = [30 { href: "/", label: fr ? "Estimer" : "Estimate" },31 { href: "/ka", label: fr ? "Ka · agent IA" : "Ka · AI agent" },32 { href: "/parc", label: fr ? "Parc immobilier" : "Portfolio" },33 { href: "/stats", label: fr ? "Statistiques" : "Statistics" },34 { href: "/methodologie", label: fr ? "Méthodologie" : "Methodology" },35 ];36 const isActive = (href: string) =>37 href === "/" ? path === "/" : path.startsWith(href);3839 return (40 <>41 {/* nav pilule — desktop */}42 <nav className="hidden items-center gap-1 md:flex">43 {items.map((it) => (44 <Link45 key={it.href}46 href={it.href}47 className={`vp-display inline-flex min-h-[38px] items-center rounded-full border-[1.5px] px-3.5 py-1.5 text-[13.5px] font-bold transition-all ${48 isActive(it.href)49 ? "border-ink bg-ink text-lime"50 : "border-transparent text-ink-2 hover:border-ink hover:text-ink"51 }`}52 >53 {it.label}54 </Link>55 ))}56 </nav>5758 {/* ---- menu mobile : checkbox cachée + peer-checked, zéro JS requis ---- */}59 <div className="md:hidden">60 <input61 ref={box}62 type="checkbox"63 id="vp-menu"64 className="peer sr-only"65 onChange={onToggle}66 aria-label="Menu"67 />68 {/* hamburger (label = bouton) */}69 <label70 htmlFor="vp-menu"71 aria-hidden="true"72 className="flex h-11 w-11 cursor-pointer flex-col items-center justify-center gap-[5px] rounded-full border-[1.5px] border-ink bg-surface active:bg-lime-soft"73 >74 <span className="h-[2px] w-[18px] bg-ink" />75 <span className="h-[2px] w-[18px] bg-ink" />76 <span className="h-[2px] w-[18px] bg-ink" />77 </label>7879 {/* panneau plein écran — sibling du checkbox, affiché via peer-checked */}80 <div81 className="fixed inset-0 z-[3000] hidden flex-col overflow-y-auto peer-checked:flex"82 style={{ background: "var(--paper)" }}83 role="dialog"84 aria-modal="true"85 aria-label="Menu"86 >87 {/* barre du haut : marque + fermer */}88 <div89 className="flex h-16 shrink-0 items-center justify-between border-b-2 border-ink px-4"90 style={{ background: "var(--paper)" }}91 >92 <Link93 href="/"94 className="vp-display flex items-center text-[22px] font-bold leading-none tracking-[-0.04em]"95 >96 Vrai97 <span className="ml-1 rotate-[-2deg] rounded-md bg-ink px-[7px] pb-1 pt-[2px] text-lime">98 Prix99 </span>100 </Link>101 <label102 htmlFor="vp-menu"103 aria-label={fr ? "Fermer le menu" : "Close menu"}104 className="vp-display flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border-[1.5px] border-ink bg-ink text-[17px] font-bold text-lime"105 >106 ✕107 </label>108 </div>109110 {/* liens géants */}111 <nav className="flex flex-1 flex-col justify-center px-6 py-8">112 {items.map((it, i) => (113 <Link114 key={it.href}115 href={it.href}116 className="group flex items-baseline gap-4 border-b-[1.5px] border-dashed border-[rgba(20,24,20,0.18)] py-5 last:border-0"117 >118 <span className="vp-mono text-[12px] font-bold text-green">119 0{i + 1}120 </span>121 <span122 className={`vp-display text-[29px] font-bold uppercase leading-none tracking-[-0.03em] ${123 isActive(it.href)124 ? "rounded-md bg-ink px-2.5 py-1 text-lime"125 : "text-ink group-active:text-green-deep"126 }`}127 >128 {it.label}129 </span>130 </Link>131 ))}132 </nav>133134 {/* pied du menu */}135 <div className="shrink-0 border-t-2 border-ink px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4">136 <button137 onClick={() => setLang(fr ? "en" : "fr")}138 className="vp-mono mb-3 rounded-full border-[1.5px] border-ink bg-surface px-4 py-2 text-[12px] font-bold uppercase tracking-[0.08em]"139 >140 {fr ? "English version" : "Version française"}141 </button>142 <div className="vp-mono flex flex-wrap gap-x-5 gap-y-1.5 text-[10.5px] uppercase tracking-[0.06em] text-ink-3">143 <Link href="/conditions">{fr ? "Conditions" : "Terms"}</Link>144 <Link href="/confidentialite">145 {fr ? "Confidentialité" : "Privacy"}146 </Link>147 <span>contact@spboucher.ai</span>148 </div>149 </div>150 </div>151 </div>152 </>153 );154}155