SPB Git

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%
5.9 KB · 167 lines tsx
Raw Blame History
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