SPB Git

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

9.6 KB · 273 lines tsx
1// ==============================================================================2// Author: Simon-Pierre Boucher <contact@spboucher.ai>3// File:   App.tsx4// Desc:   Layout global (header + ticker en direct + footer encre) et routage —5//         même patron que le App.tsx de Lou·Ka.6// ==============================================================================7import { useEffect, useState } from "react";8import { NavLink, Route, Routes, useLocation } from "react-router-dom";9import { AccountProvider, useAccount } from "./account";10import {11  fetchAuthConfig, fetchFacets, fetchSources, fetchStats, logout,12  registerSourceNames, sourceName,13} from "./api";14import {15  IcoChart, IcoCompass, IcoFolder, IcoPlate, IcoSearch,16} from "./components/Icons";17import { LogoIcon } from "./components/Logo";18import GroupeKaBadge from "./ka/GroupeKaBadge";19import KaFooter from "./ka/KaFooter";20import ContactPage from "./pages/Contact";21import FavorisPage from "./pages/Favoris";22import Home from "./pages/Home";23import RestoPage from "./pages/Resto";24import SourcesPage from "./pages/Sources";25import StatsPage from "./pages/Stats";2627/** Bouton « Se connecter avec KA » ou avatar + menu compte (hub Groupe KA). */28function AccountMenu() {29  const [enabled, setEnabled] = useState(false);30  const { me, refresh } = useAccount();31  const [menuOpen, setMenuOpen] = useState(false);3233  useEffect(() => {34    fetchAuthConfig().then((c) => setEnabled(c.ka)).catch(() => {});35  }, []);3637  if (!enabled) return null;38  if (!me) {39    return (40      <a className="login-btn" href="/api/auth/ka/login">41        <span className="ka-badge" aria-hidden="true">KA</span>42        Connexion43      </a>44    );45  }46  return (47    <div className="account">48      <button49        className="account-btn"50        onClick={() => setMenuOpen(!menuOpen)}51        aria-expanded={menuOpen}52        aria-label={`Compte : ${me.name || me.email}`}53      >54        {me.picture55          ? <img src={me.picture} alt="" referrerPolicy="no-referrer" />56          : <span className="account-initial">{(me.name || me.email).charAt(0).toUpperCase()}</span>}57      </button>58      {menuOpen && (59        <>60          <div className="account-backdrop" onClick={() => setMenuOpen(false)} aria-hidden="true" />61          <div className="account-menu" role="menu">62            <div className="account-id">63              <b>{me.name || "Mon compte"}</b>64              <span>{me.email}</span>65              {me.ka_id && <span className="account-kaid">{me.ka_id}</span>}66            </div>67            <NavLink to="/favoris" role="menuitem" className="account-link"68              onClick={() => setMenuOpen(false)}>69              Mes favoris70            </NavLink>71            <a href="https://www.groupe-ka.com/compte" target="_blank"72              rel="noopener noreferrer" role="menuitem" className="account-link">73              Mon profil Groupe KA ↗74            </a>75            <button76              role="menuitem"77              onClick={async () => { await logout(); refresh(); setMenuOpen(false); }}78            >79              Se déconnecter80            </button>81          </div>82        </>83      )}84    </div>85  );86}8788function Ticker() {89  const [items, setItems] = useState<string[]>([]);9091  useEffect(() => {92    Promise.all([fetchStats(), fetchFacets(), fetchSources()])93      .then(([stats, facets, src]) => {94        registerSourceNames(src.sources);95        const parts: string[] = [96          `${stats.restaurants} restaurants`,97          `${stats.items} plats avec prix`,98          `${stats.regions} région${stats.regions > 1 ? "s" : ""} couvertes`,99        ];100        for (const r of stats.by_region.slice(0, 6))101          parts.push(`${r.region} · ${r.n}`);102        for (const s of facets.sources.slice(0, 6))103          parts.push(`${sourceName(s.source)} · ${s.n}`);104        parts.push("Prix réels étiquetés par contexte");105        parts.push("Mise à jour automatique");106        setItems(parts);107      })108      .catch(() => setItems(["Resto-Ka — les restaurants du Québec, menus et prix"]));109  }, []);110111  if (items.length === 0) return null;112  // contenu doublé pour une boucle de défilement continue113  return (114    <div className="ticker" aria-hidden="true">115      <div className="ticker-track">116        {[...items, ...items].map((t, i) => (117          <span key={i}>{t}</span>118        ))}119      </div>120    </div>121  );122}123124const NAV_LINKS = [125  { to: "/", label: "Restos", icon: <IcoPlate size={17} />, end: true },126  { to: "/stats", label: "Stats", icon: <IcoChart size={17} />, end: false },127  { to: "/sources", label: "Sources", icon: <IcoFolder size={17} />, end: false },128  { to: "/contact", label: "Contact", icon: <IcoCompass size={17} />, end: false },129];130131function Header() {132  const [open, setOpen] = useState(false);133  const location = useLocation();134135  // fermer le menu à chaque navigation + verrouiller le défilement en dessous136  useEffect(() => { setOpen(false); }, [location]);137  useEffect(() => {138    document.body.style.overflow = open ? "hidden" : "";139    return () => { document.body.style.overflow = ""; };140  }, [open]);141142  return (143    <>144      <header className="header">145        <div className="container header-inner">146          <NavLink to="/" className="brand" aria-label="Resto-Ka — accueil">147            <LogoIcon size={30} />148            Resto-<span className="ka">Ka</span>149            <span className="brand-tag">Chaque resto, chaque plat, chaque prix.</span>150          </NavLink>151          <span className="hdr-badge"><GroupeKaBadge /></span>152          <nav className="nav" aria-label="Navigation principale">153            {NAV_LINKS.map((l) => (154              <NavLink key={l.to} to={l.to} end={l.end}155                className={({ isActive }) => (isActive ? "active" : "")}>156                {l.label}157              </NavLink>158            ))}159          </nav>160          <AccountMenu />161          <button162            className={`menu-btn ${open ? "open" : ""}`}163            aria-expanded={open}164            aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}165            onClick={() => setOpen(!open)}166          >167            <span /><span /><span />168          </button>169        </div>170171        {/* menu déroulant mobile */}172        <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Menu mobile">173          {NAV_LINKS.map((l, i) => (174            <NavLink175              key={l.to}176              to={l.to}177              end={l.end}178              style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}179              className={({ isActive }) => `mm-link ${isActive ? "active" : ""}`}180              onClick={() => setOpen(false)}181            >182              <span className="mm-ico" aria-hidden="true">{l.icon}</span>183              {l.label}184              <span className="mm-arrow" aria-hidden="true">→</span>185            </NavLink>186          ))}187          <div className="mm-badge"><GroupeKaBadge /></div>188          <div className="mm-foot">189            Agrégateur indépendant — menus et prix captés aux sources, chaque190            fiche renvoie au resto original et affiche le contexte du prix.191          </div>192        </div>193      </header>194      {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}195      <Ticker />196    </>197  );198}199200/** Rangée de nav locale + footer commun Groupe KA (ka-ui/KaFooter). */201function Footer() {202  return (203    <>204      <div className="prefoot">205        <div className="container prefoot-inner">206          <nav className="prefoot-nav" aria-label="Navigation du pied de page">207            {NAV_LINKS.map((l) => (208              <NavLink key={l.to} to={l.to} end={l.end}>{l.label}</NavLink>209            ))}210            <NavLink to="/favoris">Favoris</NavLink>211          </nav>212          <div className="prefoot-osm">213            Données de découverte ©{" "}214            <a href="https://www.openstreetmap.org/copyright" target="_blank"215              rel="noopener noreferrer">contributeurs OpenStreetMap</a> (ODbL)216          </div>217        </div>218      </div>219      <KaFooter siteId="resto-ka" />220    </>221  );222}223224/** Barre de navigation inférieure (mobile) — icône active en orange. */225function MobileTabBar() {226  const location = useLocation();227  const tabs = [228    { to: "/", label: "Rechercher", icon: <IcoSearch size={20} />, on: location.pathname === "/" },229    { to: "/stats", label: "Stats", icon: <IcoChart size={20} />, on: location.pathname.startsWith("/stats") },230    { to: "/sources", label: "Sources", icon: <IcoFolder size={20} />, on: location.pathname.startsWith("/sources") },231  ];232  return (233    <nav className="tabbar tabbar-3" aria-label="Navigation mobile">234      {tabs.map((t) => (235        <NavLink key={t.label} to={t.to} className={() => (t.on ? "active" : "")}>236          {t.icon}237          {t.label}238        </NavLink>239      ))}240    </nav>241  );242}243244export default function App() {245  return (246    <AccountProvider>247      <Header />248      <main>249        <Routes>250          <Route path="/" element={<Home />} />251          <Route path="/resto/:uid" element={<RestoPage />} />252          <Route path="/favoris" element={<FavorisPage />} />253          <Route path="/stats" element={<StatsPage />} />254          <Route path="/sources" element={<SourcesPage />} />255          <Route path="/contact" element={<ContactPage />} />256          <Route257            path="*"258            element={259              <div className="notice container">260                <div className="big"><IcoCompass size={40} /></div>261                <h2>Page introuvable</h2>262                <p>Le lien demandé n'existe pas.</p>263              </div>264            }265          />266        </Routes>267      </main>268      <Footer />269      <MobileTabBar />270    </AccountProvider>271  );272}273
Files · 749c169c68aa2af83c9abd2dbd38ec22f518b42e
Menu

Theme