TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import { useMemo, useState } from "react";3import { computeMortgage, minDownPayment, transferDuty } from "@/lib/mortgage";4import { fmtPrice, fmtPct } from "@/lib/format";5import { Notice, Stat } from "@/components/ui";67const DEFAULT_RATE = 4.89; // hypothèse éditable — aucun taux « réel » n'est importé ; l'utilisateur saisit le taux de son prêteur89function F({ label, value, set, step = 1, suffix = "" }: { label: string; value: number; set: (v: number) => void; step?: number; suffix?: string }): React.ReactElement {10 return ( <label className="block text-sm"><span className="tv-label">{label}</span><div className="relative mt-1"><input type="number" inputMode="decimal" step={step} value={value} onChange={(e) => set(Number(e.target.value))} className="tv-chip w-full !pl-3 !pr-8 tabular-nums" />{suffix && <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-ink-3">{suffix}</span>}</div></label>11 );12}1314export function MortgageWidget({ price: p0 = 500000, city, compact = false, taxes: t0, schoolTaxes: s0, condoFees: c0 }: { price?: number; city?: string | null; compact?: boolean; taxes?: number; schoolTaxes?: number; condoFees?: number }) {15 const [price, setPrice] = useState(p0);16 const [dpPct, setDpPct] = useState(20);17 const [rate, setRate] = useState(DEFAULT_RATE);18 const [amort, setAmort] = useState(25);19 const [freq, setFreq] = useState<12 | 26 | 52>(12);20 const [taxes, setTaxes] = useState(t0 ?? Math.round(p0 * 0.009));21 const [school, setSchool] = useState(s0 ?? Math.round(p0 * 0.0007));22 const [ins, setIns] = useState(90);23 const [condo, setCondo] = useState(c0 ?? 0);24 const [heat, setHeat] = useState(150);25 const dp = Math.round(price * dpPct / 100);26 const r = useMemo(() => computeMortgage({ price, downPayment: dp, rateAnnualPct: rate, amortYears: amort, paymentsPerYear: freq, municipalTaxAnnual: taxes, schoolTaxAnnual: school, insuranceMonthly: ins, condoFeesMonthly: condo, heatingMonthly: heat }), [price, dp, rate, amort, freq, taxes, school, ins, condo, heat]);27 const isMtl = /montr[ée]al/i.test(city ?? "");28 const duty = transferDuty(price, isMtl);29 const closing = duty + 1500 + 1000; // notaire + inspection (ordres de grandeur éditables mentalement)30 return (31 <div className="grid lg:grid-cols-[1fr_1.1fr] gap-5">32 <div className="tv-card p-4 grid grid-cols-2 gap-3">33 <F label="Prix" value={price} set={setPrice} step={1000} suffix="$" />34 <label className="block text-sm"><span className="tv-label">Mise de fonds</span><div className="mt-1 flex items-center gap-2"><input type="range" min={5} max={50} step={1} value={dpPct} onChange={(e) => setDpPct(Number(e.target.value))} className="flex-1 accent-[#e0263c]" aria-label="Mise de fonds en pourcentage" /><span className="text-xs tabular-nums w-24 text-right">{dpPct} % · {fmtPrice(dp, { compact: true })}</span></div></label>35 <F label="Taux d'intérêt (hypothèse)" value={rate} set={setRate} step={0.05} suffix="%" />36 <label className="block text-sm"><span className="tv-label">Amortissement</span><select value={amort} onChange={(e) => setAmort(Number(e.target.value))} className="tv-chip w-full mt-1 appearance-none">{[15, 20, 25, 30].map((y) => <option key={y} value={y}>{y} ans</option>)}</select></label>37 <label className="block text-sm"><span className="tv-label">Fréquence</span><select value={freq} onChange={(e) => setFreq(Number(e.target.value) as 12 | 26 | 52)} className="tv-chip w-full mt-1 appearance-none"><option value={12}>Mensuelle</option><option value={26}>Aux deux semaines</option><option value={52}>Hebdomadaire</option></select></label>38 {!compact && <F label="Taxes municipales (an)" value={taxes} set={setTaxes} step={50} suffix="$" />}39 {!compact && <F label="Taxes scolaires (an)" value={school} set={setSchool} step={10} suffix="$" />}40 <F label="Assurance habitation (mois)" value={ins} set={setIns} step={5} suffix="$" />41 <F label="Frais de condo (mois)" value={condo} set={setCondo} step={10} suffix="$" />42 {!compact && <F label="Chauffage (mois)" value={heat} set={setHeat} step={10} suffix="$" />}43 </div>44 <div className="space-y-3">45 <div className="grid grid-cols-2 gap-3">46 <Stat label={`Versement ${freq === 12 ? "mensuel" : freq === 26 ? "aux deux semaines" : "hebdomadaire"}`} value={fmtPrice(r.payment)} sub={freq !== 12 ? `≈ ${fmtPrice(r.paymentMonthlyEq)} / mois` : `taux ${rate.toLocaleString("fr-CA")} %, ${amort} ans`} />47 <Stat label="Coût mensuel total estimé" value={fmtPrice(r.monthlyAllIn)} sub="hypothèque + taxes + assurance + condo + chauffage" tone="info" />48 <Stat label="Prêt total" value={fmtPrice(r.loan)} sub={r.cmhcPremium > 0 ? `dont prime d'assurance ${fmtPrice(r.cmhcPremium)} (${r.cmhcRatePct} %)` : "sans assurance prêt (mise de fonds ≥ 20 %)"} />49 <Stat label="Intérêts sur la durée" value={fmtPrice(r.totalInterest)} sub={`total remboursé ${fmtPrice(r.totalPaid)}`} />50 <Stat label="Test de résistance" value={fmtPrice(r.stressTest.payment)} sub={`versement mensuel à ${r.stressTest.qualifyingRatePct.toLocaleString("fr-CA")} % (taux + 2 pts, min 5,25 %)`} />51 <Stat label="Frais de clôture (ordre de grandeur)" value={fmtPrice(closing)} sub={`droits de mutation ${fmtPrice(duty)}${isMtl ? " (barème Montréal)" : ""} + notaire ~1 500 $ + inspection ~1 000 $`} />52 </div>53 {r.warnings.map((w) => <Notice key={w} tone="warn">{w}</Notice>)}54 <Notice>Calcul illustratif à partir de vos hypothèses (taux composé semestriellement, barème d'assurance prêt standard, mise de fonds minimale {fmtPct(minDownPayment(price) / price, 1, false)}). Ce n'est ni une offre ni un conseil financier : vérifiez les taux et conditions auprès d'un prêteur.</Notice>55 {!compact && (56 <details className="tv-card p-3 text-sm"><summary className="cursor-pointer font-semibold">Tableau d'amortissement annuel</summary>57 <table className="w-full mt-2 text-xs"><thead><tr className="text-ink-3 text-left"><th className="py-1 font-medium">Année</th><th className="py-1 font-medium text-right">Intérêts</th><th className="py-1 font-medium text-right">Capital</th><th className="py-1 font-medium text-right">Solde</th></tr></thead><tbody>{r.schedule.map((s) => <tr key={s.year} className="border-t border-line"><td className="py-1">{s.year}</td><td className="py-1 text-right tabular-nums">{fmtPrice(s.interest)}</td><td className="py-1 text-right tabular-nums">{fmtPrice(s.principal)}</td><td className="py-1 text-right tabular-nums">{fmtPrice(s.balance)}</td></tr>)}</tbody></table>58 </details>59 )}60 </div>61 </div>62 );63}64