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// pages/Stats.tsx : simple live dashboard — totals, by state, by type,5// recent syncs (/api/stats).6// -----------------------------------------------------------------------------7import { useEffect, useState } from "react";8import { Stats, fetchStats, sourceName } from "../api";910const n = (x: number | null | undefined) =>11 x == null ? "—" : Math.round(x).toLocaleString("en-US");12const usd = (x: number | null | undefined) =>13 x == null ? "—" : "$" + Math.round(x).toLocaleString("en-US");1415function ago(ts: number): string {16 const s = Math.max(0, Math.floor(Date.now() / 1000 - (ts > 1e12 ? ts / 1000 : ts)));17 if (s < 3600) return `${Math.round(s / 60)} min ago`;18 if (s < 172800) return `${Math.round(s / 3600)} h ago`;19 return `${Math.round(s / 86400)} d ago`;20}2122export default function StatsPage() {23 const [stats, setStats] = useState<Stats | null>(null);24 const [error, setError] = useState<string | null>(null);2526 useEffect(() => {27 fetchStats().then(setStats).catch((e) => setError(String(e)));28 }, []);2930 return (31 <div className="container stats-page" style={{ display: "grid", gap: 22, minWidth: 0 }}>32 <header style={{ minWidth: 0 }}>33 <span className="kicker">The aggregated market</span>34 <h1 className="stats-title">Statistics</h1>35 <p className="sub" style={{ marginBottom: 14 }}>36 Live dashboard of the homes for sale aggregated by Home-Ka — volumes37 and prices computed on the platform's real data (source duplicates38 excluded, "price on request" excluded).39 </p>40 </header>4142 {error && (43 <div className="notice">44 <h2>Statistics unavailable</h2>45 <p>{error}</p>46 </div>47 )}48 {!stats && !error && <div className="notice">Loading…</div>}4950 {stats && (51 <>52 {/* --- totals row -------------------------------------------------- */}53 <section aria-label="Key totals" className="tiles">54 <div className="tile hero-tile">55 <div className="tile-v">{n(stats.total)}</div>56 <div className="tile-k">Homes for sale</div>57 </div>58 <div className="tile">59 <div className="tile-v">{n(stats.properties)}</div>60 <div className="tile-k">Physical properties tracked</div>61 </div>62 <div className="tile">63 <div className="tile-v">{n(stats.states)}</div>64 <div className="tile-k">States</div>65 </div>66 <div className="tile">67 <div className="tile-v">{n(stats.cities)}</div>68 <div className="tile-k">Cities</div>69 </div>70 <div className="tile">71 <div className="tile-v">{n(stats.sources)}</div>72 <div className="tile-k">Sources</div>73 </div>74 <div className="tile">75 <div className="tile-v">{usd(stats.avg_price)}</div>76 <div className="tile-k">Average price</div>77 </div>78 <div className="tile">79 <div className="tile-v">{usd(stats.min_price)}</div>80 <div className="tile-k">Lowest price</div>81 </div>82 <div className="tile">83 <div className="tile-v">{usd(stats.max_price)}</div>84 <div className="tile-k">Highest price</div>85 </div>86 </section>8788 {/* --- by state ----------------------------------------------------- */}89 {stats.by_state.length > 0 && (90 <section aria-label="By state">91 <h2>By state</h2>92 <div className="q-table-wrap">93 <table className="q-table">94 <thead>95 <tr><th>State</th><th>Homes</th><th>Average price</th></tr>96 </thead>97 <tbody>98 {stats.by_state.map((r) => (99 <tr key={r.state}>100 <td>{r.state}</td>101 <td className="num">{n(r.n)}</td>102 <td className="num">{usd(r.avg_price)}</td>103 </tr>104 ))}105 </tbody>106 </table>107 </div>108 </section>109 )}110111 {/* --- by property type ---------------------------------------------- */}112 {stats.by_type.length > 0 && (113 <section aria-label="By property type">114 <h2>By property type</h2>115 <div className="q-table-wrap">116 <table className="q-table">117 <thead>118 <tr><th>Type</th><th>Homes</th><th>Average price</th></tr>119 </thead>120 <tbody>121 {stats.by_type.map((r) => (122 <tr key={r.property_type}>123 <td>{r.property_type}</td>124 <td className="num">{n(r.n)}</td>125 <td className="num">{usd(r.avg_price)}</td>126 </tr>127 ))}128 </tbody>129 </table>130 </div>131 </section>132 )}133134 {/* --- recent syncs --------------------------------------------------- */}135 {(stats.recent_syncs?.length ?? 0) > 0 && (136 <section aria-label="Recent syncs">137 <h2>Recent syncs</h2>138 <div className="q-table-wrap">139 <table className="q-table">140 <thead>141 <tr>142 <th>Source</th><th>When</th><th>Found</th>143 <th>Added</th><th>Removed</th><th>Status</th>144 </tr>145 </thead>146 <tbody>147 {stats.recent_syncs!.map((s, i) => (148 <tr key={`${s.source}-${s.ts}-${i}`}>149 <td>{sourceName(s.source)}</td>150 <td>{ago(s.ts)}</td>151 <td className="num">{n(s.found)}</td>152 <td className="num">{n(s.added)}</td>153 <td className="num">{n(s.removed)}</td>154 <td>155 {s.ok156 ? <span className="adm-badge adm-ok">OK</span>157 : <span className="adm-badge adm-err" title={s.message}>Error</span>}158 </td>159 </tr>160 ))}161 </tbody>162 </table>163 </div>164 </section>165 )}166 </>167 )}168169 <p className="stats-foot">170 Updated automatically — statistics are computed on the aggregated171 listings. Every listing links back to the original announcement at172 the source.173 </p>174 </div>175 );176}177