// ============================================================================== // Author: Simon-Pierre Boucher // File: App.tsx // Desc: Layout global (header + ticker en direct + footer encre) et routage — // même patron que le App.tsx de Lou·Ka. // ============================================================================== import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { NavLink, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { AccountProvider, useAccount } from "./account"; import { fetchAuthConfig, fetchFacets, fetchSources, fetchStats, logout, registerSourceNames, sourceName, } from "./api"; import { IcoChart, IcoCompass, IcoFolder, IcoHeart, IcoPlate, IcoSearch, } from "./components/Icons"; import { FavProvider } from "./favorites"; import { LogoIcon } from "./components/Logo"; import GroupeKaBadge from "./ka/GroupeKaBadge"; import KaFooter from "./ka/KaFooter"; import KaTabbar from "./ka/KaTabbar"; import ContactPage from "./pages/Contact"; import FavorisPage from "./pages/Favoris"; import Home from "./pages/Home"; import RestoPage from "./pages/Resto"; import SourcesPage from "./pages/Sources"; import StatsPage from "./pages/Stats"; import VillePage, { VillesPage } from "./pages/Ville"; /** Bouton « Se connecter avec KA » ou avatar + menu compte (hub Groupe KA). */ function AccountMenu() { const [enabled, setEnabled] = useState(false); const { me, refresh } = useAccount(); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { fetchAuthConfig().then((c) => setEnabled(c.ka)).catch(() => {}); }, []); // menu ouvert : fermeture à Escape (le tap extérieur passe par le backdrop) useEffect(() => { if (!menuOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [menuOpen]); if (!enabled) return null; if (!me) { return ( Connexion ); } return (
{/* Backdrop + menu rendus à la racine via createPortal : le backdrop-filter du header re-parentait le position:fixed (voile de 66 px) et le stacking context du header (z-50) plaquait le menu SOUS la tabbar (85) et le fab (80). Le menu devient fixed, ancré sous le header (voir .account-menu dans styles.css). */} {menuOpen && createPortal( <>
setMenuOpen(false)} aria-hidden="true" />
{me.name || "Mon compte"} {me.email} {me.ka_id && {me.ka_id}}
setMenuOpen(false)}> Mes favoris Mon profil Groupe KA ↗
, document.body, )}
); } function Ticker() { const [items, setItems] = useState([]); useEffect(() => { Promise.all([fetchStats(), fetchFacets(), fetchSources()]) .then(([stats, facets, src]) => { registerSourceNames(src.sources); const parts: string[] = [ `${stats.restaurants} restaurants`, `${stats.items} plats avec prix`, `${stats.regions} région${stats.regions > 1 ? "s" : ""} couvertes`, ]; for (const r of stats.by_region.slice(0, 6)) parts.push(`${r.region} · ${r.n}`); for (const s of facets.sources.slice(0, 6)) parts.push(`${sourceName(s.source)} · ${s.n}`); parts.push("Prix réels étiquetés par contexte"); parts.push("Mise à jour automatique"); setItems(parts); }) .catch(() => setItems(["Resto-Ka — les restaurants du Québec, menus et prix"])); }, []); if (items.length === 0) return null; // contenu doublé pour une boucle de défilement continue return ( ); } const NAV_LINKS = [ { to: "/", label: "Restos", icon: , end: true }, { to: "/favoris", label: "Favoris", icon: , end: false }, { to: "/stats", label: "Stats", icon: , end: false }, { to: "/sources", label: "Sources", icon: , end: false }, { to: "/contact", label: "Contact", icon: , end: false }, ]; function Header() { const [open, setOpen] = useState(false); const location = useLocation(); // fermer le menu à chaque navigation + verrouiller le défilement en dessous useEffect(() => { setOpen(false); }, [location]); useEffect(() => { document.documentElement.classList.toggle("ka-scroll-lock", open); return () => { document.documentElement.classList.remove("ka-scroll-lock"); }; }, [open]); return ( <>
Resto-Ka Chaque resto, chaque plat, chaque prix.
{/* menu déroulant mobile */}
{/* ✕ fixe du panneau : visible peu importe le scroll, le burger du header est masqué à l'ouverture */} {NAV_LINKS.map((l, i) => ( `mm-link ${isActive ? "active" : ""}`} onClick={() => setOpen(false)} > {l.label} ))}
Agrégateur indépendant — menus et prix captés aux sources, chaque fiche renvoie au resto original et affiche le contexte du prix.
{open &&
setOpen(false)} aria-hidden="true" />} ); } /** Rangée de nav locale + footer commun Groupe KA (ka-ui/KaFooter). */ function Footer() { return ( <>
Données de découverte ©{" "} contributeurs OpenStreetMap (ODbL)
); } /** Barre de navigation mobile commune Groupe KA (ka-ui/nav — KA Tabbar v1) : Découvrir · Recherche · Villes · Favoris, accent orange signature. */ function MobileTabBar() { const location = useLocation(); const navigate = useNavigate(); const p = location.pathname; const focusSearch = () => { const go = !document.getElementById("f-q"); if (go) navigate("/"); window.setTimeout(() => { const el = document.getElementById("f-q") as HTMLInputElement | null; if (el) { el.scrollIntoView({ block: "center", behavior: "smooth" }); el.focus({ preventScroll: true }); } }, go ? 140 : 0); }; return ( ); } export default function App() { return (
} /> } /> } /> } /> } /> } /> } /> } />

Page introuvable

Le lien demandé n'existe pas.

} />