spb/auto-ka Public
Python 82.8%
TypeScript 11.9%
CSS 5.1%
1// -----------------------------------------------------------------------------2// Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// Vehicle.tsx : fiche véhicule — galerie, specs, historique de prix, similaires5// -----------------------------------------------------------------------------6import { useEffect, useState } from "react";7import { Link, useParams } from "react-router-dom";8import { VehicleDetail, fetchVehicle, fmtDate, fmtKm, fmtPrice } from "../api";9import VehicleCard from "../components/VehicleCard";1011export default function VehiclePage() {12 const { uid } = useParams<{ uid: string }>();13 const [v, setV] = useState<VehicleDetail | null>(null);14 const [error, setError] = useState(false);15 const [img, setImg] = useState(0);1617 useEffect(() => {18 setV(null); setError(false); setImg(0);19 if (uid) fetchVehicle(uid).then(setV).catch(() => setError(true));20 window.scrollTo(0, 0);21 }, [uid]);2223 if (error)24 return (25 <div className="notice container">26 <div className="big">🚗</div>27 <h2>Véhicule introuvable</h2>28 <p>Il a peut-être été vendu — l'inventaire évolue chaque jour.</p>29 <p><Link to="/" className="btn ghost">← Retour à la recherche</Link></p>30 </div>31 );3233 if (!v)34 return (35 <div className="container vdetail">36 <div className="skeleton" style={{ height: 60, marginBottom: 20 }} />37 <div className="vd-cols">38 <div className="skeleton" style={{ height: 460 }} />39 <div className="skeleton" style={{ height: 460 }} />40 </div>41 </div>42 );4344 const prevPrice = v.price_history.find(45 (h) => h.price != null && v.price != null && h.price !== v.price46 )?.price;4748 const specs: [string, string][] = [49 ["Année", v.year ? String(v.year) : "—"],50 ["Kilométrage", fmtKm(v.mileage_km)],51 ["Transmission", v.transmission || "—"],52 ["Carburant", v.fuel || "—"],53 ["Motricité", v.drivetrain || "—"],54 ["Carrosserie", v.body_type || "—"],55 ["Moteur", v.engine || "—"],56 ["Couleur ext.", v.exterior_color || "—"],57 ["Couleur int.", v.interior_color || "—"],58 ["Portes", v.doors ? String(v.doors) : "—"],59 ["Places", v.seats ? String(v.seats) : "—"],60 ["NIV (VIN)", v.vin || "—"],61 ["No de stock", v.stock_number || "—"],62 ].filter(([, val]) => val !== "—") as [string, string][];6364 return (65 <div className="container vdetail">66 <p style={{ marginTop: 0 }}>67 <Link to="/" className="mono" style={{ fontSize: 12 }}>← Retour à la recherche</Link>68 </p>69 <div className="vd-head">70 <div>71 <span className="kicker">{v.dealer_name || v.source} — {v.city}{v.region ? ` · ${v.region}` : ""}</span>72 <h1>{v.title}</h1>73 </div>74 <div className="vd-price">75 <div className="p">{fmtPrice(v.price)}</div>76 {prevPrice != null && v.price != null && prevPrice > v.price && (77 <div className="was">{fmtPrice(prevPrice)}</div>78 )}79 </div>80 </div>8182 <div className="vd-cols">83 <div>84 <div className="gallery">85 <div className="main">86 {v.images.length > 0 ? (87 <a href={v.images[img]} target="_blank" rel="noreferrer">88 <img src={v.images[img]} alt={v.title} />89 </a>90 ) : (91 <div className="nopic" style={{ display: "grid", placeItems: "center", height: "100%", fontSize: 60 }}>🚗</div>92 )}93 </div>94 {v.images.length > 1 && (95 <div className="thumbs">96 {v.images.map((u, i) => (97 <img98 key={u}99 src={u}100 alt={`photo ${i + 1}`}101 loading="lazy"102 className={i === img ? "sel" : ""}103 onClick={() => setImg(i)}104 />105 ))}106 </div>107 )}108 </div>109110 {v.description && (111 <div className="panel" style={{ marginTop: 18 }}>112 <h3>📝 Description du concessionnaire</h3>113 <div className="desc">{v.description}</div>114 </div>115 )}116 </div>117118 <div>119 <div className="panel">120 <h3>🔧 Caractéristiques</h3>121 <table className="spec-table">122 <tbody>123 {specs.map(([k, val]) => (124 <tr key={k}>125 <td>{k}</td>126 <td>{val}</td>127 </tr>128 ))}129 </tbody>130 </table>131 <a className="cta-source" href={v.url} target="_blank" rel="noreferrer">132 Voir chez {v.dealer_name || "le concessionnaire"} →133 </a>134 {v.carfax_url && (135 <a136 className="cta-source"137 style={{ background: "var(--surface)", color: "var(--ink)", marginTop: 10 }}138 href={v.carfax_url}139 target="_blank"140 rel="noreferrer"141 >142 📋 Rapport Carfax143 </a>144 )}145 <div className="cta-note">146 annonce originale — prix et disponibilité confirmés à la source147 </div>148 </div>149150 {v.features.length > 0 && (151 <div className="panel">152 <h3>✨ Équipements ({v.features.length})</h3>153 <div className="feat-list">154 {v.features.map((f) => (155 <span key={f} className="spec-chip">{f}</span>156 ))}157 </div>158 </div>159 )}160161 {v.price_history.length > 1 && (162 <div className="panel">163 <h3>📉 Historique de prix</h3>164 <div className="price-history">165 {v.price_history.map((h, i) => {166 const next = v.price_history[i + 1];167 const dir =168 next?.price != null && h.price != null169 ? h.price < next.price ? "down" : h.price > next.price ? "up" : ""170 : "";171 return (172 <div key={h.ts} className="row">173 <span>{fmtDate(h.ts)}</span>174 <span className={dir}>{fmtPrice(h.price)}</span>175 </div>176 );177 })}178 </div>179 </div>180 )}181182 <div className="panel">183 <h3>ℹ️ Suivi Auto-Ka</h3>184 <table className="spec-table">185 <tbody>186 <tr><td>Repéré le</td><td>{fmtDate(v.first_seen)}</td></tr>187 <tr><td>Vérifié le</td><td>{fmtDate(v.updated_at)}</td></tr>188 <tr><td>Statut</td><td>{v.active ? "En vente" : "Retiré / vendu"}</td></tr>189 </tbody>190 </table>191 </div>192 </div>193 </div>194195 {v.similar.length > 0 && (196 <section style={{ marginTop: 40 }}>197 <span className="kicker">Comparer</span>198 <h2 style={{ margin: "8px 0 18px" }}>199 {v.make} {v.model} similaires au Québec200 </h2>201 <div className="vgrid">202 {v.similar.map((s) => (203 <VehicleCard key={s.uid} v={s} />204 ))}205 </div>206 </section>207 )}208 </div>209 );210}211