spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// App.tsx : layout global (header + ticker en direct + footer encre) et routage5// -----------------------------------------------------------------------------6import { useEffect, useState } from "react";7import { NavLink, Route, Routes, useLocation } from "react-router-dom";8import { fetchFacets, fetchSources, fetchStats, registerSourceNames, sourceName } from "./api";9import CookieConsent from "./components/CookieConsent";10import Home from "./pages/Home";11import ListingPage from "./pages/Listing";12import BotPage from "./pages/Bot";13import PasserellePage from "./pages/Passerelle";14import PrivacyPage from "./pages/Privacy";15import SourcesPage from "./pages/Sources";16import StatsPage from "./pages/Stats";1718function Ticker() {19 const [items, setItems] = useState<string[]>([]);2021 useEffect(() => {22 Promise.all([fetchStats(), fetchFacets(), fetchSources()])23 .then(([stats, facets, src]) => {24 registerSourceNames(src.sources);25 const parts: string[] = [26 `${stats.total} logements actifs`,27 `Québec ${stats.quebec ?? 0}`,28 `Lévis ${stats.levis ?? 0}`,29 ];30 if ((stats.montreal ?? 0) > 0) parts.push(`Grand Montréal ${stats.montreal}`);31 if (stats.avg_price != null)32 parts.push(`Loyer moyen ${Math.round(stats.avg_price)} $`);33 for (const s of facets.sources.slice(0, 10))34 parts.push(`${sourceName(s.source)} · ${s.n}`);35 parts.push("Mise à jour automatique");36 setItems(parts);37 })38 .catch(() => setItems(["Lou-Ka — logements à louer au Québec"]));39 }, []);4041 if (items.length === 0) return null;42 // contenu doublé pour une boucle de défilement continue43 return (44 <div className="ticker" aria-hidden="true">45 <div className="ticker-track">46 {[...items, ...items].map((t, i) => (47 <span key={i}>{t}</span>48 ))}49 </div>50 </div>51 );52}5354const NAV_LINKS = [55 { to: "/", label: "Logements", icon: "🏠", end: true },56 { to: "/?view=carte", label: "Carte", icon: "◈", end: false, force: true },57 { to: "/stats", label: "Stats", icon: "📊", end: false },58 { to: "/sources", label: "Sources", icon: "🗂", end: false },59 { to: "/confidentialite", label: "Confidentialité", icon: "🔒", end: false },60];6162function Header() {63 const [open, setOpen] = useState(false);64 const location = useLocation();6566 // fermer le menu à chaque navigation + verrouiller le défilement en dessous67 useEffect(() => { setOpen(false); }, [location]);68 useEffect(() => {69 document.body.style.overflow = open ? "hidden" : "";70 return () => { document.body.style.overflow = ""; };71 }, [open]);7273 return (74 <>75 <header className="header">76 <div className="container header-inner">77 <NavLink to="/" className="brand" aria-label="Lou-Ka — accueil">78 Lou<span className="ka">Ka</span>79 <span className="brand-tag">Québec · Lévis · Montréal — toujours à jour</span>80 </NavLink>81 <nav className="nav" aria-label="Navigation principale">82 <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>83 Logements84 </NavLink>85 <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>86 Stats87 </NavLink>88 <NavLink to="/sources" className={({ isActive }) => (isActive ? "active" : "")}>89 Sources90 </NavLink>91 </nav>92 <button93 className={`menu-btn ${open ? "open" : ""}`}94 aria-expanded={open}95 aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}96 onClick={() => setOpen(!open)}97 >98 <span /><span /><span />99 </button>100 </div>101102 {/* menu déroulant mobile */}103 <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Menu mobile">104 {NAV_LINKS.map((l, i) => (105 <NavLink106 key={l.to}107 to={l.to}108 end={l.end}109 style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}110 className={({ isActive }) =>111 `mm-link ${isActive && !l.force ? "active" : ""}`}112 onClick={() => setOpen(false)}113 >114 <span className="mm-ico" aria-hidden="true">{l.icon}</span>115 {l.label}116 <span className="mm-arrow" aria-hidden="true">→</span>117 </NavLink>118 ))}119 <div className="mm-foot">120 Agrégateur indépendant — mis à jour automatiquement, chaque fiche121 renvoie à l'annonce originale.122 </div>123 </div>124 </header>125 {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}126 <Ticker />127 </>128 );129}130131function Footer() {132 return (133 <footer className="footer">134 <div className="container">135 <div className="fbrand">136 Lou<span className="ka">Ka</span>137 </div>138 <div className="frow">139 <div>140 <b>Agrégateur indépendant</b> de logements à louer dans la province de Québec.141 Les annonces proviennent des sites publics des gestionnaires immobiliers et sont142 rafraîchies automatiquement — chaque fiche renvoie vers l'annonce originale.143 </div>144 </div>145 <div className="fmono">146 © {new Date().getFullYear()} Simon-Pierre Boucher — contact@spboucher.ai147 {" · "}148 <NavLink to="/confidentialite">Confidentialité</NavLink>149 {" · "}150 <button151 className="flink"152 onClick={() => window.dispatchEvent(new Event("louka:openConsent"))}153 >154 Gérer mes témoins155 </button>156 </div>157 </div>158 </footer>159 );160}161162export default function App() {163 return (164 <>165 <Header />166 <main>167 <Routes>168 <Route path="/" element={<Home />} />169 <Route path="/logement/:uid" element={<ListingPage />} />170 <Route path="/stats" element={<StatsPage />} />171 <Route path="/sources" element={<SourcesPage />} />172 <Route path="/confidentialite" element={<PrivacyPage />} />173 <Route path="/passerelle/:uid" element={<PasserellePage />} />174 <Route path="/bot" element={<BotPage />} />175 <Route176 path="*"177 element={178 <div className="notice container">179 <div className="big">🧭</div>180 <h2>Page introuvable</h2>181 <p>Le lien demandé n'existe pas.</p>182 </div>183 }184 />185 </Routes>186 </main>187 <Footer />188 <CookieConsent />189 </>190 );191}192