spb/ora-ka Public
Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)
Python 80%
TypeScript 12.9%
CSS 6.8%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (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 { Ico } from "./components/Icons";7import { useEffect, useState } from "react";8import { NavLink, Route, Routes, useLocation } from "react-router-dom";9import { fetchFacets, fetchSources, fetchStats, registerSourceNames, sourceName } from "./api";10import Home from "./pages/Home";11import ListingPage from "./pages/Listing";12import SourcesPage from "./pages/Sources";13import StatsPage from "./pages/Stats";1415function Ticker() {16 const [items, setItems] = useState<string[]>([]);1718 useEffect(() => {19 Promise.all([fetchStats(), fetchFacets(), fetchSources()])20 .then(([stats, facets, src]) => {21 registerSourceNames(src.sources);22 const parts: string[] = [`${stats.total.toLocaleString("fr-CA")} propriétés à vendre`];23 if (stats.cities) parts.push(`${stats.cities} villes`);24 if (stats.avg_price != null)25 parts.push(`Prix moyen ${Math.round(stats.avg_price).toLocaleString("fr-CA")} $`);26 for (const s of facets.sources.slice(0, 12))27 parts.push(`${sourceName(s.source)} · ${s.n}`);28 parts.push("Mise à jour automatique");29 setItems(parts);30 })31 .catch(() => setItems(["Immo-Ka — propriétés à vendre au Québec"]));32 }, []);3334 if (items.length === 0) return null;35 return (36 <div className="ticker" aria-hidden="true">37 <div className="ticker-track">38 {[...items, ...items].map((t, i) => (39 <span key={i}>{t}</span>40 ))}41 </div>42 </div>43 );44}4546const NAV_LINKS = [47 { to: "/", label: "Propriétés", icon: "home", end: true },48 { to: "/?view=carte", label: "Carte", icon: "map", end: false, force: true },49 { to: "/stats", label: "Stats", icon: "chart", end: false },50 { to: "/agences", label: "Agences", icon: "building", end: false },51];5253function Header() {54 const [open, setOpen] = useState(false);55 const location = useLocation();5657 useEffect(() => { setOpen(false); }, [location]);58 useEffect(() => {59 document.body.style.overflow = open ? "hidden" : "";60 return () => { document.body.style.overflow = ""; };61 }, [open]);6263 return (64 <>65 <header className="header">66 <div className="container header-inner">67 <NavLink to="/" className="brand" aria-label="Immo-Ka — accueil">68 Immo<span className="ka">Ka</span>69 <span className="brand-tag">Toutes les agences du Québec — toujours à jour</span>70 </NavLink>71 <nav className="nav" aria-label="Navigation principale">72 <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>73 Propriétés74 </NavLink>75 <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>76 Stats77 </NavLink>78 <NavLink to="/agences" className={({ isActive }) => (isActive ? "active" : "")}>79 Agences80 </NavLink>81 </nav>82 <button83 className={`menu-btn ${open ? "open" : ""}`}84 aria-expanded={open}85 aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}86 onClick={() => setOpen(!open)}87 >88 <span /><span /><span />89 </button>90 </div>9192 <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Menu mobile">93 {NAV_LINKS.map((l, i) => (94 <NavLink95 key={l.to}96 to={l.to}97 end={l.end}98 style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}99 className={({ isActive }) => `mm-link ${isActive && !l.force ? "active" : ""}`}100 onClick={() => setOpen(false)}101 >102 <span className="mm-ico" aria-hidden="true"><Ico name={l.icon} size={19} /></span>103 {l.label}104 <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>105 </NavLink>106 ))}107 <div className="mm-foot">108 Agrégateur indépendant — mis à jour automatiquement, chaque fiche109 renvoie à l'annonce originale de l'agence.110 </div>111 </div>112 </header>113 {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}114 <Ticker />115 </>116 );117}118119function Footer() {120 return (121 <footer className="footer">122 <div className="container">123 <div className="fbrand">124 Immo<span className="ka">Ka</span>125 </div>126 <div className="frow">127 <div>128 <b>Agrégateur indépendant</b> des propriétés à vendre dans la province de Québec.129 Les annonces proviennent des sites publics des agences de courtage (RE/MAX, Sutton,130 Via Capitale, Proprio Direct, Barnes, M Immobilier…) et sont rafraîchies131 automatiquement — chaque fiche renvoie vers l'annonce originale.132 </div>133 </div>134 <div className="fmono">135 © {new Date().getFullYear()} Simon-Pierre Boucher — contact@spboucher.ai136 </div>137 </div>138 </footer>139 );140}141142export default function App() {143 return (144 <>145 <Header />146 <main>147 <Routes>148 <Route path="/" element={<Home />} />149 <Route path="/propriete/:uid" element={<ListingPage />} />150 <Route path="/stats" element={<StatsPage />} />151 <Route path="/agences" element={<SourcesPage />} />152 <Route153 path="*"154 element={155 <div className="notice container">156 <div className="big">🧭</div>157 <h2>Page introuvable</h2>158 <p>Le lien demandé n'existe pas.</p>159 </div>160 }161 />162 </Routes>163 </main>164 <Footer />165 </>166 );167}168