spb/immo-ka Public
Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 66.4%
TypeScript 19.9%
CSS 13.2%
HTML 0.5%
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 { useEffect, useState } from "react";7import { NavLink, Route, Routes, useLocation } from "react-router-dom";8import { fetchFacets, fetchSources, fetchStats, registerSourceNames, sourceName } from "./api";9import Home from "./pages/Home";10import ListingPage from "./pages/Listing";11import SourcesPage from "./pages/Sources";12import StatsPage from "./pages/Stats";1314function Ticker() {15 const [items, setItems] = useState<string[]>([]);1617 useEffect(() => {18 Promise.all([fetchStats(), fetchFacets(), fetchSources()])19 .then(([stats, facets, src]) => {20 registerSourceNames(src.sources);21 const parts: string[] = [`${stats.total.toLocaleString("fr-CA")} propriétés à vendre`];22 if (stats.cities) parts.push(`${stats.cities} villes`);23 if (stats.avg_price != null)24 parts.push(`Prix moyen ${Math.round(stats.avg_price).toLocaleString("fr-CA")} $`);25 for (const s of facets.sources.slice(0, 12))26 parts.push(`${sourceName(s.source)} · ${s.n}`);27 parts.push("Mise à jour automatique");28 setItems(parts);29 })30 .catch(() => setItems(["Immo-Ka — propriétés à vendre au Québec"]));31 }, []);3233 if (items.length === 0) return null;34 return (35 <div className="ticker" aria-hidden="true">36 <div className="ticker-track">37 {[...items, ...items].map((t, i) => (38 <span key={i}>{t}</span>39 ))}40 </div>41 </div>42 );43}4445const NAV_LINKS = [46 { to: "/", label: "Propriétés", icon: "🏠", end: true },47 { to: "/?view=carte", label: "Carte", icon: "◈", end: false, force: true },48 { to: "/stats", label: "Stats", icon: "📊", end: false },49 { to: "/agences", label: "Agences", icon: "🗂", end: false },50];5152function Header() {53 const [open, setOpen] = useState(false);54 const location = useLocation();5556 useEffect(() => { setOpen(false); }, [location]);57 useEffect(() => {58 document.body.style.overflow = open ? "hidden" : "";59 return () => { document.body.style.overflow = ""; };60 }, [open]);6162 return (63 <>64 <header className="header">65 <div className="container header-inner">66 <NavLink to="/" className="brand" aria-label="Immo-Ka — accueil">67 Immo<span className="ka">Ka</span>68 <span className="brand-tag">Toutes les agences du Québec — toujours à jour</span>69 </NavLink>70 <nav className="nav" aria-label="Navigation principale">71 <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>72 Propriétés73 </NavLink>74 <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>75 Stats76 </NavLink>77 <NavLink to="/agences" className={({ isActive }) => (isActive ? "active" : "")}>78 Agences79 </NavLink>80 </nav>81 <button82 className={`menu-btn ${open ? "open" : ""}`}83 aria-expanded={open}84 aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}85 onClick={() => setOpen(!open)}86 >87 <span /><span /><span />88 </button>89 </div>9091 <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Menu mobile">92 {NAV_LINKS.map((l, i) => (93 <NavLink94 key={l.to}95 to={l.to}96 end={l.end}97 style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}98 className={({ isActive }) => `mm-link ${isActive && !l.force ? "active" : ""}`}99 onClick={() => setOpen(false)}100 >101 <span className="mm-ico" aria-hidden="true">{l.icon}</span>102 {l.label}103 <span className="mm-arrow" aria-hidden="true">→</span>104 </NavLink>105 ))}106 <div className="mm-foot">107 Agrégateur indépendant — mis à jour automatiquement, chaque fiche108 renvoie à l'annonce originale de l'agence.109 </div>110 </div>111 </header>112 {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}113 <Ticker />114 </>115 );116}117118function Footer() {119 return (120 <footer className="footer">121 <div className="container">122 <div className="fbrand">123 Immo<span className="ka">Ka</span>124 </div>125 <div className="frow">126 <div>127 <b>Agrégateur indépendant</b> des propriétés à vendre dans la province de Québec.128 Les annonces proviennent des sites publics des agences de courtage (RE/MAX, Sutton,129 Via Capitale, Proprio Direct, Barnes, M Immobilier…) et sont rafraîchies130 automatiquement — chaque fiche renvoie vers l'annonce originale.131 </div>132 </div>133 <div className="fmono">134 © {new Date().getFullYear()} Simon-Pierre Boucher — contact@spboucher.ai135 </div>136 </div>137 </footer>138 );139}140141export default function App() {142 return (143 <>144 <Header />145 <main>146 <Routes>147 <Route path="/" element={<Home />} />148 <Route path="/propriete/:uid" element={<ListingPage />} />149 <Route path="/stats" element={<StatsPage />} />150 <Route path="/agences" element={<SourcesPage />} />151 <Route152 path="*"153 element={154 <div className="notice container">155 <div className="big">🧭</div>156 <h2>Page introuvable</h2>157 <p>Le lien demandé n'existe pas.</p>158 </div>159 }160 />161 </Routes>162 </main>163 <Footer />164 </>165 );166}167