TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import { useState } from "react";3import type { ToveliEstimate } from "@/lib/estimate";4import { MapView } from "@/components/map-view";5import { EstimateBreakdown } from "@/components/estimate-breakdown";6import { fmtPrice } from "@/lib/format";7import { Notice, Stat } from "@/components/ui";8export function ManualEstimate() {9 const [pt, setPt] = useState<[number, number]>([46.8139, -71.208]);10 const [cat, setCat] = useState("maison"); const [area, setArea] = useState(""); const [year, setYear] = useState(""); const [lot, setLot] = useState("");11 const [res, setRes] = useState<ToveliEstimate | null>(null); const [err, setErr] = useState<string | null>(null); const [busy, setBusy] = useState(false);12 const run = async () => {13 setBusy(true); setErr(null); setRes(null);14 const r = await fetch("/api/estimate", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ lat: pt[0], lng: pt[1], category: cat, floorAreaSqft: area ? Number(area) : null, yearBuilt: year ? Number(year) : null, landAreaSqft: lot ? Number(lot) : null, label: "Point choisi sur la carte" }) });15 const d = await r.json(); setBusy(false);16 if (!r.ok) setErr(d.error ?? "Erreur"); else setRes(d);17 };18 return (19 <div className="grid lg:grid-cols-2 gap-4">20 <div className="space-y-3">21 <div className="h-64 rounded-card overflow-hidden border border-line relative"><MapView points={[{ uid: "pt", lat: pt[0], lng: pt[1], price: 0 }]} center={pt} zoom={13} onMove={(b) => setPt([(b[1] + b[3]) / 2, (b[0] + b[2]) / 2])} /><span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-full text-brand text-2xl pointer-events-none" aria-hidden>📍</span></div>22 <p className="text-xs text-ink-3">Déplacez la carte pour placer le centre sur la propriété. Position : {pt[0].toFixed(5)}, {pt[1].toFixed(5)}</p>23 <div className="grid grid-cols-2 gap-3 text-sm">24 <label><span className="tv-label">Type</span><select value={cat} onChange={(e) => setCat(e.target.value)} className="tv-chip w-full mt-1 appearance-none"><option value="maison">Maison</option><option value="condo">Condo</option><option value="plex">Plex</option><option value="chalet">Chalet</option></select></label>25 <label><span className="tv-label">Superficie habitable (pi²)</span><input value={area} onChange={(e) => setArea(e.target.value)} type="number" inputMode="numeric" className="tv-chip w-full mt-1 !pl-3" placeholder="1 500" /></label>26 <label><span className="tv-label">Année de construction</span><input value={year} onChange={(e) => setYear(e.target.value)} type="number" inputMode="numeric" className="tv-chip w-full mt-1 !pl-3" placeholder="1995" /></label>27 <label><span className="tv-label">Terrain (pi²)</span><input value={lot} onChange={(e) => setLot(e.target.value)} type="number" inputMode="numeric" className="tv-chip w-full mt-1 !pl-3" placeholder="6 000" /></label>28 </div>29 <button type="button" onClick={run} disabled={busy} className="tv-btn tv-btn-primary">{busy ? "Calcul…" : "Estimer à partir des comparables"}</button>30 {err && <Notice tone="warn">{err}</Notice>}31 </div>32 <div>33 {res ? (<div className="space-y-3">34 <div className="grid grid-cols-2 gap-3"><Stat label="Estimation" value={fmtPrice(res.result.estimate)} sub={`${fmtPrice(res.result.low)} – ${fmtPrice(res.result.high)}`} /><Stat label="Confiance" value={`${res.result.confidenceLevel} · ${res.result.confidencePct} %`} sub={`${res.result.nCompsUsed} comparables`} /></div>35 <EstimateBreakdown e={res} />36 <Notice>{res.method.disclaimer}</Notice>37 </div>) : <div className="tv-card p-8 text-center text-sm text-ink-3 h-full grid place-items-center">Le résultat s'affichera ici, avec la décomposition et les limites de la méthode.</div>}38 </div>39 </div>40 );41}42