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// pages/Stats.tsx : statistiques des deux univers — tuiles louer/acheter,6// loyers et prix moyens par ville et par type (données /api/stats).7// -----------------------------------------------------------------------------8import { useEffect, useState } from "react";9import { Link } from "react-router-dom";10import { Stats, Tx, TxStats, fetchStats, slugify } from "../api";1112const fmt = (n: number | null | undefined, suffix = "") =>13 n == null ? "…" : Math.round(n).toLocaleString("fr-CA") + suffix;1415function Bars({ rows, tx, unit }: {16 rows: { key: string; n: number; avg: number | null; href?: string }[];17 tx: Tx; unit?: string;18}) {19 const max = Math.max(1, ...rows.map((r) => r.n));20 return (21 <div className="hbars">22 {rows.map((r) => (23 <div className="hbar-row" key={r.key}>24 <div className="hbar-label">{r.href ? <Link to={r.href}>{r.key}</Link> : r.key}</div>25 <div className="hbar-track"><span className="hbar-fill" style={{ width: `${(r.n / max) * 100}%` }} /></div>26 <div className="hbar-value">27 {r.n.toLocaleString("fr-CA")}28 {r.avg != null && <em> · {fmt(r.avg)} ${tx === "louer" ? "/mois" : ""}{unit ?? ""}</em>}29 </div>30 </div>31 ))}32 </div>33 );34}3536function Univers({ tx, s }: { tx: Tx; s: TxStats }) {37 const louer = tx === "louer";38 return (39 <div className="stats-univers">40 <h2>41 <span className={`nav-dot ${tx}`} aria-hidden="true" />42 {louer ? "Louer" : "Acheter"}43 </h2>44 <div className="tiles" style={{ marginTop: 16 }}>45 <div className="tile hero-tile">46 <div className="tile-v">{fmt(s.total)}</div>47 <div className="tile-k">{louer ? "logements à louer" : "propriétés à vendre"}</div>48 </div>49 <div className="tile">50 <div className="tile-v">{fmt(s.avg_price, " $")}</div>51 <div className="tile-k">{louer ? "loyer moyen /mois" : "prix moyen demandé"}</div>52 </div>53 <div className="tile">54 <div className="tile-v">{fmt(s.cities)}</div>55 <div className="tile-k">villes couvertes</div>56 </div>57 <div className="tile">58 <div className="tile-v">{fmt(s.sources)}</div>59 <div className="tile-k">sources agrégées</div>60 </div>61 </div>62 <h3 style={{ fontSize: 15, margin: "8px 0 10px" }}>Top villes</h3>63 <Bars tx={tx} rows={s.top_cities.map((c) => ({64 key: c.city, n: c.n, avg: c.avg_price, href: `/${tx}/${slugify(c.city)}`,65 }))} />66 <h3 style={{ fontSize: 15, margin: "18px 0 10px" }}>Par type</h3>67 <Bars tx={tx} rows={s.top_types.map((t) => ({68 key: t.type, n: t.n, avg: t.avg_price, href: `/${tx}/type/${slugify(t.type)}`,69 }))} />70 </div>71 );72}7374export default function StatsPage() {75 const [stats, setStats] = useState<Stats | null>(null);7677 useEffect(() => {78 fetchStats().then(setStats).catch(() => {});79 }, []);8081 return (82 <div className="container stats-page">83 <span className="kicker">Les deux marchés, côte à côte</span>84 <h1 className="stats-title">Statistiques</h1>85 <p className="sub">86 Portrait en direct des deux univers agrégés par Toit-Ka : la location87 (données Lou-Ka) et la vente (données Immo-Ka), mises à jour en continu.88 </p>8990 {stats ? (91 <div className="stats-duo">92 <Univers tx="louer" s={stats.louer} />93 <Univers tx="acheter" s={stats.acheter} />94 </div>95 ) : (96 <div className="notice">Chargement…</div>97 )}9899 <p className="stats-foot">100 Mise à jour automatique — chaque fiche renvoie à l'annonce originale de la source.101 </p>102 </div>103 );104}105