Python 67%
TypeScript 18.2%
CSS 14.4%
1// -----------------------------------------------------------------------------2// House-Ka — Homes-for-sale aggregator (Canada outside Québec, Ontario first)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// App.tsx : global layout (header + live ticker + footer) and 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 { kaLogin, useAccount } from "./account";11import AccountPage from "./pages/Account";12import AgenciesPage from "./pages/Agencies";13import ContactPage from "./pages/Contact";14import Home from "./pages/Home";15import { PrivacyPage, TermsPage } from "./pages/Legal";16import ListingPage from "./pages/Listing";17import RatesPage from "./pages/Rates";18import StatsPage from "./pages/Stats";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-CA")} homes for sale`];28 if (stats.cities) parts.push(`${stats.cities.toLocaleString("en-CA")} cities & towns`);29 if (stats.avg_price != null)30 parts.push(`Average price $${Math.round(stats.avg_price).toLocaleString("en-CA")}`);31 for (const s of facets.sources.slice(0, 12))32 parts.push(`${sourceName(s.source)} · ${s.n.toLocaleString("en-CA")}`);33 parts.push("Continuously updated");34 setItems(parts);35 })36 .catch(() => setItems(["House-Ka — homes for sale across Canada"]));37 }, []);3839 if (items.length === 0) return null;40 return (41 <div className="ticker" aria-hidden="true">42 <div className="ticker-track">43 {[...items, ...items].map((t, i) => (44 <span key={i}>{t}</span>45 ))}46 </div>47 </div>48 );49}5051/** Header account zone: "Sign in with KA" or a link to the profile. */52function AccountNav() {53 const { user, enabled, loaded } = useAccount();54 if (!loaded || !enabled) return null;55 if (user) {56 return (57 <NavLink58 to="/account"59 className={({ isActive }) => `ka-acct ${isActive ? "active" : ""}`}60 >61 {user.picture ? (62 <img className="ka-acct-pic" src={user.picture} alt="" referrerPolicy="no-referrer" />63 ) : (64 <Ico name="people" size={16} />65 )}66 {user.name ? user.name.split(" ")[0] : "Account"}67 </NavLink>68 );69 }70 return (71 <div className="ka-auth">72 <button className="ka-login" onClick={() => kaLogin()}>73 Sign in with <b>KA</b>74 </button>75 <a76 className="ka-signup"77 href="https://www.groupe-ka.com/connexion"78 target="_blank"79 rel="noopener noreferrer"80 >81 Create an account82 </a>83 </div>84 );85}8687/** Same account zone, mobile-menu version. */88function AccountMobile({ close }: { close: () => void }) {89 const { user, enabled, loaded } = useAccount();90 if (!loaded || !enabled) return null;91 if (user) {92 return (93 <NavLink to="/account" className="mm-link" onClick={close}>94 <span className="mm-ico" aria-hidden="true"><Ico name="people" size={19} /></span>95 My account {user.name ? `— ${user.name.split(" ")[0]}` : ""}96 <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>97 </NavLink>98 );99 }100 return (101 <div className="mm-auth">102 <button className="ka-login" onClick={() => kaLogin()}>103 Sign in with <b>KA</b>104 </button>105 <a106 className="ka-signup"107 href="https://www.groupe-ka.com/connexion"108 target="_blank"109 rel="noopener noreferrer"110 >111 Create an account112 </a>113 </div>114 );115}116117const NAV_LINKS = [118 { to: "/", label: "Homes", icon: "home", end: true },119 { to: "/?view=map", label: "Map", icon: "map", end: false, force: true },120 { to: "/rates", label: "Mortgage rates", icon: "trendup", end: false },121 { to: "/stats", label: "Stats", icon: "chart", end: false },122 { to: "/agencies", label: "Brokerages", icon: "building", end: false },123 { to: "/contact", label: "Contact", icon: "arrow", end: false },124];125126function Header() {127 const [open, setOpen] = useState(false);128 const location = useLocation();129130 useEffect(() => { setOpen(false); }, [location]);131 useEffect(() => {132 document.documentElement.classList.toggle("ka-scroll-lock", open);133 return () => { document.documentElement.classList.remove("ka-scroll-lock"); };134 }, [open]);135136 return (137 <>138 <header className="header">139 <div className="container header-inner">140 <NavLink to="/" className="brand" aria-label="House-Ka — home">141 House<span className="ka">Ka</span>142 </NavLink>143 <span className="brand-tag">A Groupe KA service</span>144 <nav className="nav" aria-label="Main navigation">145 <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>146 Homes147 </NavLink>148 <NavLink to="/rates" className={({ isActive }) => (isActive ? "active" : "")}>149 Rates150 </NavLink>151 <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>152 Stats153 </NavLink>154 <NavLink to="/agencies" className={({ isActive }) => (isActive ? "active" : "")}>155 Brokerages156 </NavLink>157 <NavLink to="/contact" className={({ isActive }) => (isActive ? "active" : "")}>158 Contact159 </NavLink>160 </nav>161 <div className="header-acct">162 <AccountNav />163 </div>164 <button165 className={`menu-btn ${open ? "open" : ""}`}166 aria-expanded={open}167 aria-label={open ? "Close the menu" : "Open the menu"}168 onClick={() => setOpen(!open)}169 >170 <span /><span /><span />171 </button>172 </div>173174 <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Mobile menu">175 {/* fixed ✕ of the panel: visible regardless of scroll; the header176 burger is hidden while the panel is open */}177 <button type="button" className="mm-close" aria-label="Close the menu"178 onClick={() => setOpen(false)}>✕</button>179 {NAV_LINKS.map((l, i) => (180 <NavLink181 key={l.to}182 to={l.to}183 end={l.end}184 style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}185 className={({ isActive }) => `mm-link ${isActive && !l.force ? "active" : ""}`}186 onClick={() => setOpen(false)}187 >188 <span className="mm-ico" aria-hidden="true"><Ico name={l.icon} size={19} /></span>189 {l.label}190 <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>191 </NavLink>192 ))}193 <AccountMobile close={() => setOpen(false)} />194 <div className="mm-foot">195 <p>196 Independent aggregator — continuously updated, every listing links197 back to the brokerage's original page.198 </p>199 </div>200 </div>201 </header>202 {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}203 <Ticker />204 </>205 );206}207208/** Bottom navigation bar (mobile) — floating pill detached from the edges,209 accent underline below the active tab. */210function MobileTabBar() {211 const location = useLocation();212 const isMap = new URLSearchParams(location.search).get("view") === "map";213 const tabs = [214 { to: "/", label: "Discover", icon: <Ico name="search" size={20} stroke={1.6} />, on: location.pathname === "/" && !isMap },215 { to: "/?view=map", label: "Map", icon: <Ico name="map" size={20} stroke={1.6} />, on: location.pathname === "/" && isMap },216 { to: "/rates", label: "Rates", icon: <Ico name="trendup" size={20} stroke={1.6} />, on: location.pathname.startsWith("/rates") },217 { to: "/account", label: "Account", icon: <Ico name="people" size={20} stroke={1.6} />, on: location.pathname.startsWith("/account") },218 ];219 return (220 <nav className="tabbar" aria-label="Mobile navigation">221 {tabs.map((t) => (222 <NavLink key={t.label} to={t.to} className={() => (t.on ? "active" : "")}>223 {t.icon}224 {t.label}225 </NavLink>226 ))}227 </nav>228 );229}230231/** House-Ka footer — ink panel, Groupe KA credit + sister sites. */232function Footer() {233 const year = new Date().getFullYear();234 const sisters = [235 { name: "Groupe·Ka", url: "https://www.groupe-ka.com", note: "the Groupe KA portal" },236 { name: "Immo·Ka", url: "https://www.immo-ka.com", note: "homes for sale in Québec" },237 { name: "Lou·Ka", url: "https://www.lou-ka.com", note: "rentals in Québec" },238 { name: "Vrai·Prix", url: "https://www.vrai-prix.com", note: "Québec market-value estimates" },239 ];240 return (241 <footer className="hk-footer" id="contact">242 <div className="container">243 <div className="hk-foot-brand">House<span className="ka">Ka</span></div>244 <p className="hk-foot-desc">245 House-Ka continuously aggregates homes for sale publicly listed by246 Canadian real-estate brokerages, teams and national networks — every247 province and territory except Québec, which lives on our sister site248 Immo-Ka. Every listing links back to the source's original page.249 House-Ka is a service of <b>Groupe KA</b>.250 </p>251 <p className="hk-foot-notice">252 House-Ka is an independent aggregator: it is not a brokerage, does not253 represent buyers or sellers, and is not affiliated with the sources it254 indexes. Prices and availability are those displayed by each source.255 </p>256 <ul className="hk-foot-sites">257 {sisters.map((s) => (258 <li key={s.url}>259 <a href={s.url} target="_blank" rel="noopener noreferrer">260 {s.name}261 </a>262 <span>{s.note}</span>263 </li>264 ))}265 </ul>266 <div className="hk-foot-legal">267 <a href="/terms">Terms of use</a>268 <a href="/privacy">Privacy</a>269 <a href="/contact">Contact</a>270 <span>© {year} Groupe-Ka</span>271 </div>272 </div>273 </footer>274 );275}276277export default function App() {278 return (279 <>280 <Header />281 <main>282 <Routes>283 <Route path="/" element={<Home />} />284 <Route path="/property/:uid" element={<ListingPage />} />285 {/* canonical SEO URL: /property/{uid}/{slug} (server 301) — without286 this route any shared/direct link would land on the 404 page */}287 <Route path="/property/:uid/:slug" element={<ListingPage />} />288 <Route path="/stats" element={<StatsPage />} />289 <Route path="/rates" element={<RatesPage />} />290 <Route path="/agencies" element={<AgenciesPage />} />291 <Route path="/contact" element={<ContactPage />} />292 <Route path="/account" element={<AccountPage />} />293 <Route path="/terms" element={<TermsPage />} />294 <Route path="/privacy" element={<PrivacyPage />} />295 <Route296 path="*"297 element={298 <div className="notice container">299 <div className="big">🧭</div>300 <h2>Page not found</h2>301 <p>The requested link does not exist.</p>302 </div>303 }304 />305 </Routes>306 </main>307 <MobileTabBar />308 <Footer />309 </>310 );311}312