SPB Git forge

spb/uqo-eval

Public
55commits 2branches 0releases
134.4 MBsize
maindefault branch
2 h agolast push
TypeScript 90.1% JavaScript 3.2% Python 3.2% CSS 1.8% HTML 1.8%
6.8 KB · 168 lines tsx
Raw Blame History
1// UQO Éval — laboratoire pédagogique d'évaluation immobilière2/**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 ? "Analyser" : "Analyze" },31    { href: "/a-vendre", label: fr ? "À vendre" : "For sale" },32    { href: "/hedonique", label: fr ? "Hédonique" : "Hedonic" },33    { href: "/carte", label: fr ? "Carte" : "Map" },34    { href: "/assistant", label: fr ? "Assistant IA" : "AI assistant" },35    { href: "/parc", label: fr ? "Parc immobilier" : "Portfolio" },36    { href: "/stats", label: fr ? "Statistiques" : "Statistics" },37    { href: "/cout", label: fr ? "Coût" : "Cost" },38    { href: "/methodologie", label: fr ? "Méthodologie" : "Methodology" },39  ];40  const isActive = (href: string) =>41    href === "/" ? path === "/" : path.startsWith(href);4243  return (44    <>45      {/* nav texte — desktop : soulignement rouge = page courante */}46      <nav className="hidden items-center gap-5 lg:flex">47        {items.map((it) => (48          <Link49            key={it.href}50            href={it.href}51            className={`vp-mono inline-flex min-h-[38px] items-center border-b-2 pt-[2px] text-[11px] font-medium uppercase tracking-[0.1em] transition-colors ${52              isActive(it.href)53                ? "border-accent text-ink"54                : "border-transparent text-ink-2 hover:text-ink"55            }`}56          >57            {it.label}58          </Link>59        ))}60      </nav>6162      {/* ---- menu mobile : checkbox cachée + peer-checked, zéro JS requis ---- */}63      <div className="lg:hidden">64        <input65          ref={box}66          type="checkbox"67          id="vp-menu"68          className="peer sr-only"69          onChange={onToggle}70          aria-label="Menu"71        />72        {/* hamburger (label = bouton) */}73        <label74          htmlFor="vp-menu"75          aria-hidden="true"76          className="flex h-11 w-11 cursor-pointer flex-col items-center justify-center gap-[5px]"77        >78          <span className="h-px w-[20px] bg-ink" />79          <span className="h-px w-[20px] bg-ink" />80          <span className="h-px w-[12px] self-center ml-[8px] bg-accent" />81        </label>8283        {/* panneau plein écran encre — sibling du checkbox, affiché via peer-checked */}84        <div85          className="vp-dark fixed inset-0 z-[var(--z-modal,900)] hidden flex-col overflow-y-auto peer-checked:flex"86          role="dialog"87          aria-modal="true"88          aria-label="Menu"89        >90          {/* barre du haut : marque + fermer */}91          <div className="flex h-14 shrink-0 items-center justify-between border-b border-[rgba(245,243,238,0.18)] px-4">92            <Link93              href="/"94              className="flex items-end gap-3"95              aria-label="UQO Éval — accueil"96            >97              {/* eslint-disable-next-line @next/next/no-img-element */}98              <img src="/uqo/uqo-blanc.svg" alt="UQO" className="h-[24px] w-auto" />99              <span className="vp-display text-[19px] font-bold leading-none tracking-[-0.04em] text-[var(--accent-bright)]">100                Éval101              </span>102            </Link>103            <label104              htmlFor="vp-menu"105              aria-label={fr ? "Fermer le menu" : "Close menu"}106              className="vp-mono flex h-11 w-11 cursor-pointer items-center justify-center text-[15px] text-[var(--paper-on-ink)]"107            >108              ✕109            </label>110          </div>111112          {/* liens géants */}113          <nav className="flex flex-1 flex-col justify-center px-6 py-8">114            {items.map((it, i) => (115              <Link116                key={it.href}117                href={it.href}118                className="group flex items-baseline gap-4 border-b border-[rgba(245,243,238,0.12)] py-[18px] last:border-0"119              >120                <span className="vp-mono text-[11px] font-medium text-[var(--accent-bright)]">121                  0{i + 1}122                </span>123                <span124                  className={`vp-display text-[28px] font-bold uppercase leading-none tracking-[-0.03em] ${125                    isActive(it.href)126                      ? "text-[var(--accent-bright)]"127                      : "text-[var(--paper-on-ink)] group-active:text-[var(--accent-bright)]"128                  }`}129                >130                  {it.label}131                </span>132                {isActive(it.href) && (133                  <span134                    aria-hidden="true"135                    className="mb-[2px] inline-block h-[6px] w-[6px] self-center bg-[var(--accent-bright)]"136                  />137                )}138              </Link>139            ))}140          </nav>141142          {/* pied du menu */}143          <div className="shrink-0 border-t border-[rgba(245,243,238,0.18)] px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4">144            <p className="vp-mono mb-4 text-[10px] uppercase tracking-[0.1em] text-[rgba(245,243,238,0.55)]">145              {fr146                ? "Laboratoire pédagogique d'évaluation immobilière — UQO"147                : "Real-estate valuation teaching laboratory — UQO"}148            </p>149            <button150              onClick={() => setLang(fr ? "en" : "fr")}151              className="vp-mono mb-4 border-b border-[var(--paper-on-ink)] pb-1 text-[12px] font-medium uppercase tracking-[0.1em] text-[var(--paper-on-ink)]"152            >153              {fr ? "English version" : "Version française"}154            </button>155            <div className="vp-mono flex flex-wrap gap-x-5 gap-y-1.5 text-[10.5px] uppercase tracking-[0.06em] text-[rgba(245,243,238,0.55)]">156              <Link href="/conditions">{fr ? "Conditions" : "Terms"}</Link>157              <Link href="/confidentialite">158                {fr ? "Confidentialité" : "Privacy"}159              </Link>160              <Link href="/contact">Contact</Link>161            </div>162          </div>163        </div>164      </div>165    </>166  );167}168