Python 49.6%
TypeScript 25.5%
CSS 24.1%
1// -----------------------------------------------------------------------------2// Home-Ka — US real-estate aggregator (Groupe KA)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// App.tsx : global layout (header + live ticker + Groupe KA footer) + routing.5// -----------------------------------------------------------------------------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 GroupeKaBadge from "./ka/GroupeKaBadge";11import KaFooter from "./ka/KaFooter";12import ContactPage from "./pages/Contact";13import Home from "./pages/Home";14import { PrivacyPage, TermsPage } from "./pages/Legal";15import ListingPage from "./pages/Listing";16import SourcesPage from "./pages/Sources";17import StatsPage from "./pages/Stats";18import AdminPage from "./pages/Admin";1920function Ticker() {21 const [items, setItems] = useState<string[]>([]);2223 useEffect(() => {24 Promise.all([fetchStats(), fetchFacets(), fetchSources()])25 .then(([stats, facets, src]) => {26 registerSourceNames(src.sources);27 const parts: string[] = [`${stats.total.toLocaleString("en-US")} homes for sale`];28 if (stats.states) parts.push(`${stats.states} states`);29 if (stats.cities) parts.push(`${stats.cities.toLocaleString("en-US")} cities`);30 if (stats.avg_price != null)31 parts.push(`Average price $${Math.round(stats.avg_price).toLocaleString("en-US")}`);32 for (const s of facets.sources.slice(0, 12))33 parts.push(`${sourceName(s.source)} · ${s.n}`);34 parts.push("Updated automatically");35 setItems(parts);36 })37 .catch(() => setItems(["Home-Ka — every home for sale in America"]));38 }, []);3940 if (items.length === 0) return null;41 return (42 <div className="ticker" aria-hidden="true">43 <div className="ticker-track">44 {[...items, ...items].map((t, i) => (45 <span key={i}>{t}</span>46 ))}47 </div>48 </div>49 );50}5152const NAV_LINKS = [53 { to: "/", label: "Homes", icon: "home", end: true },54 { to: "/?view=map", label: "Map", icon: "map", end: false, force: true },55 { to: "/stats", label: "Stats", icon: "chart", end: false },56 { to: "/sources", label: "Sources", icon: "building", end: false },57 { to: "/contact", label: "Contact", icon: "arrow", end: false },58];5960function Header() {61 const [open, setOpen] = useState(false);62 const location = useLocation();6364 useEffect(() => { setOpen(false); }, [location]);65 useEffect(() => {66 document.documentElement.classList.toggle("ka-scroll-lock", open);67 return () => { document.documentElement.classList.remove("ka-scroll-lock"); };68 }, [open]);6970 return (71 <>72 <header className="header">73 <div className="container header-inner">74 <NavLink to="/" className="brand" aria-label="Home-Ka — home">75 Home<span className="ka">Ka</span>76 </NavLink>77 <GroupeKaBadge />78 <nav className="nav" aria-label="Main navigation">79 <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>80 Homes81 </NavLink>82 <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>83 Stats84 </NavLink>85 <NavLink to="/sources" className={({ isActive }) => (isActive ? "active" : "")}>86 Sources87 </NavLink>88 <NavLink to="/contact" className={({ isActive }) => (isActive ? "active" : "")}>89 Contact90 </NavLink>91 </nav>92 <button93 className={`menu-btn ${open ? "open" : ""}`}94 aria-expanded={open}95 aria-label={open ? "Close menu" : "Open menu"}96 onClick={() => setOpen(!open)}97 >98 <span /><span /><span />99 </button>100 </div>101102 <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Mobile menu">103 <button type="button" className="mm-close" aria-label="Close menu"104 onClick={() => setOpen(false)}>✕</button>105 {NAV_LINKS.map((l, i) => (106 <NavLink107 key={l.to}108 to={l.to}109 end={l.end}110 style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}111 className={({ isActive }) => `mm-link ${isActive && !l.force ? "active" : ""}`}112 onClick={() => setOpen(false)}113 >114 <span className="mm-ico" aria-hidden="true"><Ico name={l.icon} size={19} /></span>115 {l.label}116 <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>117 </NavLink>118 ))}119 <div className="mm-foot">120 <GroupeKaBadge />121 <p>122 Independent aggregator — updated automatically; every listing123 links back to the original announcement at the source.124 </p>125 </div>126 </div>127 </header>128 {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}129 <Ticker />130 </>131 );132}133134const LOCAL_LEGAL = [135 { label: "Terms of Use (Home-Ka)", href: "/terms" },136 { label: "Privacy Policy (Home-Ka)", href: "/privacy" },137 { label: "Contact", href: "/contact" },138];139140/** Bottom navigation bar (mobile) — floating pill, same language as Lou-Ka v2. */141function MobileTabBar() {142 const location = useLocation();143 const isMap = new URLSearchParams(location.search).get("view") === "map";144 const tabs = [145 { to: "/", label: "Discover", icon: <Ico name="search" size={20} stroke={1.6} />, on: location.pathname === "/" && !isMap },146 { to: "/?view=map", label: "Map", icon: <Ico name="map" size={20} stroke={1.6} />, on: location.pathname === "/" && isMap },147 { to: "/stats", label: "Stats", icon: <Ico name="chart" size={20} stroke={1.6} />, on: location.pathname.startsWith("/stats") },148 { to: "/sources", label: "Sources", icon: <Ico name="building" size={20} stroke={1.6} />, on: location.pathname.startsWith("/sources") },149 ];150 return (151 <nav className="tabbar" aria-label="Mobile navigation">152 {tabs.map((t) => (153 <NavLink key={t.label} to={t.to} className={() => (t.on ? "active" : "")}>154 {t.icon}155 {t.label}156 </NavLink>157 ))}158 </nav>159 );160}161162export default function App() {163 return (164 <>165 <Header />166 <main>167 <Routes>168 <Route path="/" element={<Home />} />169 <Route path="/property/:uid" element={<ListingPage />} />170 {/* SEO canonical URL: /property/{uid}/{slug} — without this route any171 shared/direct link would land on the 404 page */}172 <Route path="/property/:uid/:slug" element={<ListingPage />} />173 <Route path="/stats" element={<StatsPage />} />174 <Route path="/sources" element={<SourcesPage />} />175 <Route path="/contact" element={<ContactPage />} />176 <Route path="/terms" element={<TermsPage />} />177 <Route path="/privacy" element={<PrivacyPage />} />178 {/* Admin — reachable by URL only, not linked in the nav */}179 <Route path="/admin" element={<AdminPage tab="sources" />} />180 <Route path="/admin/sources" element={<AdminPage tab="sources" />} />181 <Route path="/admin/connectors" element={<AdminPage tab="connectors" />} />182 <Route path="/admin/brokerages" element={<AdminPage tab="brokerages" />} />183 <Route184 path="*"185 element={186 <div className="notice container">187 <div className="big">🧭</div>188 <h2>Page not found</h2>189 <p>The requested link does not exist.</p>190 </div>191 }192 />193 </Routes>194 </main>195 <MobileTabBar />196 <KaFooter siteId="home-ka" localLegal={LOCAL_LEGAL} />197 </>198 );199}200