SPB Git

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