/** * ============================================================================= * QWHPI — Quebec Weekly Housing Price Index * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : web/app/page.tsx * Purpose : Overview — ticker tape, animated mega hero on a draw-in chart, * regional podium, metric tiles, pulse heatmap table. * ============================================================================= */ "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { fetchOverview, fetchSeries, fetchStats, marketReportUrl, type Observation, type OverviewRow, type SeriesStats, } from "../lib/api"; import CountUp from "../components/CountUp"; import HeroChart from "../components/HeroChart"; import ReliabilityBadge from "../components/ReliabilityBadge"; import { DashboardSkeleton } from "../components/Skeleton"; import Sparkline from "../components/Sparkline"; import Ticker from "../components/Ticker"; function Delta({ v, suffix = "%" }: { v: number | null; suffix?: string }) { if (v == null) return ; const cls = v >= 0 ? "delta up" : "delta down"; return ( {v >= 0 ? "▲" : "▼"} {Math.abs(v).toFixed(2)} {suffix} ); } function heatStyle(v: number | null, lo: number, hi: number) { if (v == null) return {}; if (v < 0) return { background: "rgba(227, 73, 72, 0.18)" }; const ramp = ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf"]; const t = hi === lo ? 0.5 : Math.max(0, Math.min(0.999, (v - lo) / (hi - lo))); const c = ramp[Math.floor(t * ramp.length)]; return { background: c, color: c === "#256abf" || c === "#3987e5" ? "#fff" : "#0b0b0b", }; } const TYPES = ["all", "unifamilial", "condo", "plex"]; const MEDALS = ["🥇", "🥈", "🥉"]; export default function Overview() { const router = useRouter(); const [stats, setStats] = useState(null); const [rows, setRows] = useState([]); const [series, setSeries] = useState([]); const [ptype, setPtype] = useState("all"); const [error, setError] = useState(null); useEffect(() => { Promise.all([ fetchStats("quebec", ptype), fetchOverview("region", ptype), fetchSeries("quebec", ptype), ]) .then(([s, o, se]) => { setStats(s); setRows(o.rows); setSeries(se.observations); setError(null); }) .catch((e) => setError(String(e))); }, [ptype]); if (error) return (
API unreachable.

Start it with make api — {error}

); if (!stats) return ; const yoys = rows.map((r) => r.yoy_pct).filter((v): v is number => v != null); const lo = Math.min(...yoys, 0); const hi = Math.max(...yoys, 1); const podium = rows.slice(0, 3); return ( <>
Month of {stats.period}

Quebec housing prices,
measured monthly, robustly.

Robust hedonic index (rolling-time-dummy) · base 2021 = 100 · uncertainty always displayed

{TYPES.map((t) => ( ))}
⤓ Market report (PDF)
QHPI-QC · 95% CI {stats.lower_95.toFixed(1)}–{stats.upper_95.toFixed(1)} {stats.at_record_high && ( · ★ record high )} YoY · since 2021

Fastest-appreciating regions

{podium.map((r, i) => (
router.push(`/explore?geography=${r.geography_id}&type=${ptype}`)}>
{MEDALS[i]} {r.geography_name}
index {r.index.toFixed(1)} {r.at_record_high ? " · at peak" : ""}
))}
{[ ["Dollar value", stats.representative_value ? : ], ["1 month", ], ["3 months", ], ["6 months", ], ["CAGR", ], ["Volatility 12m", {stats.volatility_12m_pct.toFixed(2)}%], ["Sales 12m", ], ["$ volume 12m", ${(stats.dollar_volume_12m / 1e9).toFixed(1)}B], ].map(([label, val]) => (
{label} {val}
))}

Regional pulse — {ptype} · month of {stats.period}

{rows.map((r) => ( router.push(`/explore?geography=${r.geography_id}&type=${ptype}`)}> ))}
RegionTrendIndex1m3m 6mYoYSince 2021Vs peak Sales 12m$ valueGrade
{r.geography_name} {r.at_record_high && } {r.index.toFixed(1)} {r.yoy_pct == null ? "—" : `${r.yoy_pct > 0 ? "+" : ""}${r.yoy_pct.toFixed(2)}%`} {r.drawdown_pct === 0 ? "peak" : `${r.drawdown_pct.toFixed(1)}%`} {r.volume_12m.toLocaleString()} {r.representative_value ? `$${Math.round(r.representative_value / 1000)}k` : "—"} {r.reliability_grade}

★ = record high · YoY column shaded by appreciation · click a row to explore the series · press ⌘K to jump anywhere.

); }