spb/toit-ka Public
Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com
Python 40.2%
TypeScript 39%
CSS 20.2%
HTML 0.7%
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Toit-Ka5// App.tsx : layout global (header + ticker en direct + footer encre) et routage.6// L'ACCENT DU SITE SUIT L'UNIVERS AFFICHÉ (html[data-mode]) : lime en mode7// Louer, rouge en mode Acheter, terracotta (marque) sur les pages neutres.8// -----------------------------------------------------------------------------9import { useEffect, useState } from "react";10import { Link, NavLink, Route, Routes, useLocation } from "react-router-dom";11import { fetchStats, logout, setMode } from "./api";12import { AccountProvider, useAccount } from "./account";13import { Ico, IcoHeart } from "./components/Icons";14import CategoryPage from "./pages/Category";15import Home from "./pages/Home";16import { PrivacyPage, TermsPage } from "./pages/Legal";17import ListingPage from "./pages/Listing";18import ProfilPage from "./pages/Profil";19import StatsPage from "./pages/Stats";2021/** L'accent global suit la route : /louer/* -> lime, /acheter/* -> rouge,22 * accueil -> géré par Home (selon le toggle), ailleurs -> marque terracotta. */23function ModeSync() {24 const location = useLocation();25 useEffect(() => {26 const p = location.pathname;27 if (p.startsWith("/louer")) setMode("louer");28 else if (p.startsWith("/acheter")) setMode("acheter");29 else if (p === "/" || p.startsWith("/annonce/")) { /* géré par la page */ }30 else setMode(null);31 }, [location]);32 return null;33}3435function Ticker() {36 const [items, setItems] = useState<string[]>([]);3738 useEffect(() => {39 fetchStats()40 .then((stats) => {41 const parts: string[] = [42 `${stats.louer.total.toLocaleString("fr-CA")} logements à louer`,43 `${stats.acheter.total.toLocaleString("fr-CA")} propriétés à vendre`,44 ];45 if (stats.louer.avg_price != null)46 parts.push(`Loyer moyen ${Math.round(stats.louer.avg_price).toLocaleString("fr-CA")} $`);47 if (stats.acheter.avg_price != null)48 parts.push(`Prix moyen ${Math.round(stats.acheter.avg_price).toLocaleString("fr-CA")} $`);49 for (const c of stats.louer.top_cities.slice(0, 5))50 parts.push(`Louer à ${c.city} · ${c.n}`);51 for (const c of stats.acheter.top_cities.slice(0, 5))52 parts.push(`Acheter à ${c.city} · ${c.n}`);53 parts.push("Mise à jour automatique");54 setItems(parts);55 })56 .catch(() => setItems(["Toit-Ka — louer ou acheter un toit au Québec"]));57 }, []);5859 if (items.length === 0) return null;60 return (61 <div className="ticker" aria-hidden="true">62 <div className="ticker-track">63 {[...items, ...items].map((t, i) => (64 <span key={i}>{t}</span>65 ))}66 </div>67 </div>68 );69}7071function AuthWidget() {72 const { user, enabled, refresh } = useAccount();73 const [open, setOpen] = useState(false);7475 if (!user && !enabled) return null;76 if (!user)77 return (78 <a className="auth-btn" href={`/api/auth/ka/login?next=${encodeURIComponent(location.pathname)}`}>79 <span className="auth-kab" aria-hidden="true">KA</span>80 KA ID81 </a>82 );83 return (84 <div className="auth-user">85 <button className="auth-avatar" onClick={() => setOpen(!open)}86 aria-label={`Compte de ${user.name}`} aria-expanded={open}>87 {user.picture88 ? <img src={user.picture} alt="" referrerPolicy="no-referrer" />89 : <span>{(user.name || user.email || "?").slice(0, 1).toUpperCase()}</span>}90 </button>91 {open && (92 <div className="auth-menu" role="menu">93 <div className="auth-id">94 <b>{user.name}</b>95 <span>{user.email}</span>96 </div>97 {user.ka_id && (98 <div className="auth-note" style={{ fontFamily: "monospace", letterSpacing: "0.08em" }}>99 KA-ID : {user.ka_id}100 </div>101 )}102 <div className="auth-note">Compte Groupe KA — le même KA-ID sur toutes les plateformes du groupe</div>103 <Link className="auth-menu-link" to="/profil" onClick={() => setOpen(false)}>104 Mon profil105 </Link>106 <a className="auth-menu-link" href="https://www.groupe-ka.com/compte"107 target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>108 <IcoHeart size={13} filled /> Mes favoris — Mon univers Ka ↗109 </a>110 <button onClick={() => logout().then(() => { refresh(); setOpen(false); })}>111 Se déconnecter112 </button>113 </div>114 )}115 </div>116 );117}118119const NAV_LINKS = [120 { to: "/?tx=louer", label: "Louer", icon: "key", dot: "louer" },121 { to: "/?tx=acheter", label: "Acheter", icon: "home", dot: "acheter" },122 { to: "/stats", label: "Stats", icon: "chart", dot: "" },123 { to: "/profil", label: "Mon profil", icon: "people", dot: "" },124];125126function Header() {127 const [open, setOpen] = useState(false);128 const location = useLocation();129 const tx = new URLSearchParams(location.search).get("tx") ?? "louer";130131 useEffect(() => { setOpen(false); }, [location]);132 useEffect(() => {133 document.body.style.overflow = open ? "hidden" : "";134 return () => { document.body.style.overflow = ""; };135 }, [open]);136137 const navActive = (label: string) => {138 if (label === "Louer") return location.pathname === "/" && tx === "louer";139 if (label === "Acheter") return location.pathname === "/" && tx === "acheter";140 return false;141 };142143 return (144 <>145 <header className="header">146 <div className="container header-inner">147 <NavLink to="/" className="brand" aria-label="Toit-Ka — accueil">148 Toit<span className="ka">Ka</span>149 <span className="brand-tag">Louer ou acheter — un seul endroit</span>150 </NavLink>151 <nav className="nav" aria-label="Navigation principale">152 <NavLink to="/?tx=louer" className={navActive("Louer") ? "active" : ""}>153 <span className="nav-dot louer" aria-hidden="true" /> Louer154 </NavLink>155 <NavLink to="/?tx=acheter" className={navActive("Acheter") ? "active" : ""}>156 <span className="nav-dot acheter" aria-hidden="true" /> Acheter157 </NavLink>158 <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>159 Stats160 </NavLink>161 </nav>162 <div className="auth-slot">163 <AuthWidget />164 </div>165 <button166 className={`menu-btn ${open ? "open" : ""}`}167 aria-expanded={open}168 aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}169 onClick={() => setOpen(!open)}170 >171 <span /><span /><span />172 </button>173 </div>174175 <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Menu mobile">176 {NAV_LINKS.map((l, i) => (177 <NavLink178 key={l.to}179 to={l.to}180 style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }}181 className={`mm-link ${navActive(l.label) ? "active" : ""}`}182 onClick={() => setOpen(false)}183 >184 <span className="mm-ico" aria-hidden="true"><Ico name={l.icon} size={19} /></span>185 {l.label}186 <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>187 </NavLink>188 ))}189 <div className="mm-foot">190 Agrégateur indépendant — location (Lou-Ka) et vente (Immo-Ka) réunies.191 Chaque fiche renvoie à l'annonce originale de la source.192 </div>193 </div>194 </header>195 {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />}196 <Ticker />197 </>198 );199}200201function Footer() {202 return (203 <footer className="footer">204 <div className="container">205 <div className="fbrand">206 Toit<span className="ka">Ka</span>207 </div>208 <div className="frow">209 <div>210 <b>Agrégateur indépendant</b> des logements à louer et des propriétés à211 vendre dans la province de Québec — la fusion de <b>Lou-Ka</b> (location)212 et d'<b>Immo-Ka</b> (vente). Les annonces proviennent des sites publics des213 gestionnaires immobiliers, agences de courtage et plateformes, rafraîchies214 automatiquement — chaque fiche renvoie vers l'annonce originale.215 </div>216 </div>217 <div className="flegal">218 <NavLink to="/conditions">Conditions d'utilisation</NavLink>219 <span aria-hidden="true"> · </span>220 <NavLink to="/confidentialite">Politique de confidentialité</NavLink>221 <span aria-hidden="true"> · </span>222 <a href="https://www.lou-ka.com" target="_blank" rel="noopener noreferrer">Lou-Ka</a>223 <span aria-hidden="true"> · </span>224 <a href="https://www.immo-ka.com" target="_blank" rel="noopener noreferrer">Immo-Ka</a>225 </div>226 <div className="fmono">227 Toit-Ka est une application du <b>Groupe-Ka</b> ·228 © {new Date().getFullYear()} Groupe-Ka —{" "}229 <a href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a>230 </div>231 </div>232 </footer>233 );234}235236export default function App() {237 return (238 <AccountProvider>239 <ModeSync />240 <Header />241 <main>242 <Routes>243 <Route path="/" element={<Home />} />244 <Route path="/annonce/:uid" element={<ListingPage />} />245 <Route path="/annonce/:uid/:slug" element={<ListingPage />} />246 {/* pages programmatiques SEO — mêmes URL que le rendu serveur */}247 <Route path="/louer/type/:type" element={<CategoryPage tx="louer" />} />248 <Route path="/louer/:ville" element={<CategoryPage tx="louer" />} />249 <Route path="/louer/:ville/:type" element={<CategoryPage tx="louer" />} />250 <Route path="/acheter/type/:type" element={<CategoryPage tx="acheter" />} />251 <Route path="/acheter/:ville" element={<CategoryPage tx="acheter" />} />252 <Route path="/acheter/:ville/:type" element={<CategoryPage tx="acheter" />} />253 <Route path="/stats" element={<StatsPage />} />254 <Route path="/profil" element={<ProfilPage />} />255 <Route path="/conditions" element={<TermsPage />} />256 <Route path="/confidentialite" element={<PrivacyPage />} />257 <Route258 path="*"259 element={260 <div className="notice container">261 <div className="big"><Ico name="alert" size={40} /></div>262 <h2>Page introuvable</h2>263 <p>Le lien demandé n'existe pas.</p>264 </div>265 }266 />267 </Routes>268 </main>269 <Footer />270 </AccountProvider>271 );272}273