Repository Public
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2"use client";3import Link from "next/link";4import SearchBox from "@/components/SearchBox";5import EstimateForm from "@/components/EstimateForm";6import { IndexSpark, locNum } from "@/components/MetricViz";7import { useLang } from "@/components/LangContext";89/** Indice de marché d'un grand type — série + variations (calculé côté serveur). */10export interface PulseItem {11 type: "unifamilial" | "condo" | "plex";12 series: { month: string; idx: number }[];13 pct12m: number | null;14 since2021: number | null;15}1617/** Vente réelle récente appariée à une unité d'évaluation. */18export interface RecentSaleItem {19 id: string;20 date: string;21 amount: number;22 street: string | null;23 city: string | null;24 propertyType: string | null;25 idProvinc: string | null;26 vsRolePct: number | null;27}2829const PULSE_LABELS: Record<PulseItem["type"], { fr: string; en: string }> = {30 unifamilial: { fr: "Unifamiliale", en: "Single-family" },31 condo: { fr: "Condo", en: "Condo" },32 plex: { fr: "Plex", en: "Plex" },33};3435function TrendBadge({ pct, label }: { pct: number | null; label: string }) {36 if (pct == null) return null;37 return (38 <span39 className={`vp-mono rounded-[4px] border border-ink px-1.5 py-0.5 text-[10px] font-bold ${40 pct >= 0 ? "bg-lime text-ink" : "bg-danger text-white"41 }`}42 >43 {label} {pct >= 0 ? "+" : ""}44 {pct.toFixed(1)} %45 </span>46 );47}4849export default function HomeClient({50 pulse,51 recent,52 salesLast30d,53}: {54 pulse: PulseItem[];55 recent: RecentSaleItem[];56 salesLast30d: number;57}) {58 const { lang, t } = useLang();59 const fr = lang === "fr";60 const money = (v: number) =>61 new Intl.NumberFormat(fr ? "fr-CA" : "en-CA", {62 style: "currency",63 currency: "CAD",64 maximumFractionDigits: 0,65 }).format(v);66 const dateFmt = (d: string) =>67 new Date(`${d}T12:00:00`).toLocaleDateString(fr ? "fr-CA" : "en-CA", {68 day: "numeric",69 month: "short",70 });7172 return (73 <div className="pb-6 pt-10 sm:pt-14">74 {/* ---- Héros ---- */}75 <section>76 <span className="kicker">77 {lang === "fr" ? "Québec · 2021-2026" : "Québec · 2021-2026"}78 </span>79 <h1 className="vp-display mt-3 max-w-4xl text-[clamp(34px,6.4vw,72px)] font-bold uppercase leading-[0.98] tracking-[-0.035em]">80 {lang === "fr" ? (81 <>82 La <span className="outline-txt">vraie</span> valeur.83 <br />84 <span className="hl">Sans boîte noire.</span>85 </>86 ) : (87 <>88 The <span className="outline-txt">true</span> value.89 <br />90 <span className="hl">No black box.</span>91 </>92 )}93 </h1>94 <p className="mt-5 max-w-xl text-[16.5px] text-ink-2">{t("sub")}</p>9596 <div className="mt-7 flex gap-2.5 overflow-x-auto pb-1.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">97 <span className="stat-chip">98 <span className="pulse" />99 <b>3 747 008</b> {lang === "fr" ? "propriétés" : "properties"}100 </span>101 <span className="stat-chip">102 <b>745 119</b> {lang === "fr" ? "ventes réelles" : "real sales"}103 </span>104 <span className="stat-chip">105 <b>{locNum(salesLast30d, lang, { maxFrac: 0 })}</b>{" "}106 {lang === "fr" ? "ventes captées / 30 j" : "sales captured / 30 d"}107 </span>108 <span className="stat-chip">109 <b>1 100</b> {lang === "fr" ? "municipalités" : "municipalities"}110 </span>111 <span className="stat-chip">112 MdAPE <b>11 %</b>113 </span>114 </div>115 </section>116117 {/* ---- Recherche ---- */}118 <section className="mt-9">119 <div className="vp-card p-4 sm:p-5">120 <p className="klabel mb-2 pl-0.5">121 {lang === "fr" ? "Adresse de la propriété" : "Property address"}122 </p>123 <SearchBox />124 </div>125 <p className="vp-mono mt-2.5 pl-1 text-[11px] uppercase tracking-[0.06em] text-ink-3">126 {t("dataNote")}127 </p>128 </section>129130 {/* ---- Comment ça marche (bande compacte) ---- */}131 <section className="mt-10 grid gap-3 sm:grid-cols-3">132 {(fr133 ? [134 ["01", "Cherchez l'adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."],135 ["02", "Le moteur croise deux sources", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."],136 ["03", "Fourchette + confiance", "P10-P90, indice A-D, rapport PDF — le calcul montré en entier."],137 ]138 : [139 ["01", "Search the address", "3.7M assessment units, typo-tolerant search."],140 ["02", "Two sources, crossed", "Hedonic model (690k sales) + real comparables adjusted in dollars."],141 ["03", "Range + confidence", "P10-P90, A-D index, PDF report — the full math, shown."],142 ]143 ).map(([num, title, body], i) => (144 <div key={num} className="flex items-start gap-3 rounded-[10px] border-[1.5px] border-dashed border-[rgba(20,24,20,0.3)] bg-surface-2 p-4">145 <span className="vp-mono flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-[1.5px] border-ink bg-ink text-[12px] font-bold text-lime">146 {num}147 </span>148 <div>149 <p className="vp-display text-[14.5px] font-bold uppercase tracking-[-0.01em]">150 {title}151 {i < 2 && <span className="ml-2 text-ink-3">→</span>}152 </p>153 <p className="mt-1 text-[12.5px] leading-snug text-ink-2">{body}</p>154 </div>155 </div>156 ))}157 </section>158159 {/* ---- Pouls du marché ---- */}160 <section className="mt-14">161 <span className="kicker">{fr ? "Indice Vrai-Prix" : "Vrai-Prix index"}</span>162 <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]">163 {fr ? "Le pouls du marché québécois" : "The pulse of the Québec market"}164 </h2>165 <p className="mt-2 max-w-2xl text-[14px] text-ink-2">166 {fr167 ? "L'indice mensuel calculé sur les ventes réelles — celui-là même qui ajuste les comparables de chaque estimation. Base 100 il y a 24 mois."168 : "The monthly index built from real sales — the same one adjusting every estimate's comparables. Base 100 twenty-four months ago."}169 </p>170 <div className="mt-4 grid gap-4 sm:grid-cols-3">171 {pulse.map((p) => (172 <div key={p.type} className="vp-card vp-card-hover p-5">173 <div className="flex items-center justify-between gap-2">174 <p className="vp-display text-[16px] font-bold uppercase tracking-[-0.01em]">175 {PULSE_LABELS[p.type][lang]}176 </p>177 <TrendBadge pct={p.pct12m} label={fr ? "12 mois" : "12 mo"} />178 </div>179 <div className="mt-3">180 <IndexSpark series={p.series} />181 </div>182 {p.since2021 != null && (183 <p className="vp-mono mt-2 text-[10.5px] uppercase tracking-[0.05em] text-ink-3">184 {fr ? "Depuis 2021" : "Since 2021"} :{" "}185 <b className="text-ink">186 {p.since2021 >= 0 ? "+" : ""}187 {p.since2021.toFixed(0)} %188 </b>189 </p>190 )}191 </div>192 ))}193 </div>194 </section>195196 {/* ---- Dernières ventes captées ---- */}197 {recent.length > 0 && (198 <section className="mt-14">199 <span className="kicker">200 <span className="pulse" style={{ width: 8, height: 8 }} />201 {fr ? "Flux en continu" : "Live feed"}202 </span>203 <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]">204 {fr ? "Dernières ventes réelles captées" : "Latest real sales captured"}205 </h2>206 <p className="mt-2 max-w-2xl text-[14px] text-ink-2">207 {fr208 ? "Transactions publiées, appariées automatiquement à leur unité d'évaluation. Cliquez pour voir l'estimation complète de la propriété vendue."209 : "Published transactions, automatically matched to their assessment unit. Click for the sold property's full estimate."}210 </p>211 <div className="src-wrap mt-4">212 <table className="src-table">213 <thead>214 <tr>215 <th>{fr ? "Date" : "Date"}</th>216 <th>{fr ? "Propriété" : "Property"}</th>217 <th className="text-right">{fr ? "Prix de vente" : "Sale price"}</th>218 <th className="text-right"></th>219 </tr>220 </thead>221 <tbody>222 {recent.map((s) => (223 <tr key={s.id}>224 <td className="vp-mono whitespace-nowrap text-[12px] font-bold uppercase">225 {dateFmt(s.date)}226 </td>227 <td>228 <p className="font-semibold leading-tight">229 {s.street}230 {s.city ? `, ${s.city}` : ""}231 </p>232 {s.propertyType && s.propertyType !== "indéterminé" && (233 <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.04em] text-ink-3">234 {s.propertyType}235 </p>236 )}237 </td>238 <td className="text-right">239 <span className="vp-display font-bold">{money(s.amount)}</span>240 {s.vsRolePct != null && Math.abs(s.vsRolePct) < 400 && (241 <p className="vp-mono mt-0.5 text-[10px] text-ink-3">242 {s.vsRolePct >= 0 ? "+" : "−"}243 {Math.abs(s.vsRolePct).toFixed(0)} %{" "}244 {fr ? "vs rôle" : "vs roll"}245 </p>246 )}247 </td>248 <td className="text-right">249 {s.idProvinc && (250 <Link251 href={`/estimation/${encodeURIComponent(s.idProvinc)}`}252 className="vp-mono whitespace-nowrap rounded-[5px] border-[1.5px] border-ink bg-surface-2 px-2.5 py-1.5 text-[11px] font-bold uppercase tracking-[0.04em] transition-colors hover:bg-lime"253 >254 {fr ? "Estimer" : "Estimate"} →255 </Link>256 )}257 </td>258 </tr>259 ))}260 </tbody>261 </table>262 </div>263 </section>264 )}265266 {/* ---- Formulaire manuel ---- */}267 <section className="mt-14">268 <span className="kicker">{lang === "fr" ? "Plan B" : "Plan B"}</span>269 <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]">270 {t("manualTitle")}271 </h2>272 <div className="vp-card mt-4 p-4 sm:p-6">273 <EstimateForm />274 </div>275 </section>276277 {/* ---- Manifeste transparence ---- */}278 <section className="mt-14 grid gap-4 sm:grid-cols-3">279 {(lang === "fr"280 ? [281 ["01", "Le calcul, montré", "Chaque ajustement de chaque comparable est affiché en dollars. Pas de score magique."],282 ["02", "Fourchette honnête", "Un prix unique est un mensonge statistique. On donne P10-P90 et un indice de confiance A-D."],283 ["03", "Données réelles", "Rôles fonciers officiels (MAMH) et ventes publiées — locales et récentes en priorité."],284 ]285 : [286 ["01", "The math, shown", "Every adjustment on every comparable is displayed in dollars. No magic score."],287 ["02", "Honest range", "A single price is a statistical lie. We give P10-P90 and an A-D confidence index."],288 ["03", "Real data", "Official assessment rolls (MAMH) and published sales — local and recent first."],289 ]290 ).map(([num, title, body]) => (291 <div key={num} className="vp-card vp-card-hover p-5">292 <p className="vp-mono text-[11px] font-bold text-green">{num}</p>293 <h3 className="vp-display mt-1.5 text-[17px] font-bold uppercase tracking-[-0.01em]">294 {title}295 </h3>296 <p className="mt-2 text-[13.5px] text-ink-2">{body}</p>297 </div>298 ))}299 </section>300 </div>301 );302}303