Python 61.6%
TypeScript 20.9%
CSS 11.4%
JavaScript 5.1%
HTML 1.1%
1// -----------------------------------------------------------------------------2// Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/VehicleFacts.tsx : « Le véhicule » — grille icône/valeur (année, km,5// transmission, carburant, motricité, carrosserie, moteur, portes, places,6// consommation), rangées (couleurs, NIV, stock, concessionnaire), description7// du concessionnaire (tronquée), équipements (12 + « Voir les N »), fiche8// constructeur décodée du NIV en accordéon.9// -----------------------------------------------------------------------------10import { ReactNode, useState } from "react";11import { VehicleDetail, fmtKm } from "../api";12import { Ico } from "../components/Icons";13import { Accordion, MoreButton, SectionCard, NBSP } from "./ui";1415export default function VehicleFacts({ v }: { v: VehicleDetail }) {16 const [descOpen, setDescOpen] = useState(false);17 const [allFeat, setAllFeat] = useState(false);18 const d = (v.details ?? {}) as Record<string, unknown>;19 const facts: { ico: ReactNode; v: string; l: string }[] = [];20 if (v.year) facts.push({ ico: <Ico name="calendar" size={17} />, v: String(v.year), l: "Année" });21 if (v.mileage_km != null) facts.push({ ico: <Ico name="trendup" size={17} />, v: fmtKm(v.mileage_km), l: "Kilométrage" });22 if (v.transmission) facts.push({ ico: <Ico name="layers" size={17} />, v: v.transmission, l: "Transmission" });23 if (v.fuel) facts.push({ ico: <Ico name="fuel" size={17} />, v: v.fuel, l: "Carburant" });24 if (v.drivetrain) facts.push({ ico: <Ico name="car" size={17} />, v: v.drivetrain, l: "Motricité" });25 if (v.body_type) facts.push({ ico: <Ico name="car" size={17} />, v: v.body_type, l: "Carrosserie" });26 if (v.engine) facts.push({ ico: <Ico name="hammer" size={17} />, v: v.engine, l: "Moteur" });27 if (v.doors) facts.push({ ico: <Ico name="home" size={17} />, v: String(v.doors), l: "Portes" });28 if (v.seats) facts.push({ ico: <Ico name="people" size={17} />, v: String(v.seats), l: "Places" });29 const city = d.fuel_city_l_100km, hwy = d.fuel_hwy_l_100km;30 if (city || hwy) facts.push({ ico: <Ico name="drop" size={17} />, v: `${city ?? "—"} / ${hwy ?? "—"}`, l: "L/100 km ville / route" });3132 const rows: [string, ReactNode][] = [];33 if (v.trim) rows.push(["Version", v.trim]);34 if (v.exterior_color) rows.push(["Couleur extérieure", v.exterior_color]);35 if (v.interior_color) rows.push(["Couleur intérieure", v.interior_color]);36 if (v.vin) rows.push(["NIV (VIN)", <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5 }}>{v.vin}</span>]);37 if (v.stock_number) rows.push(["Nº de stock", v.stock_number]);38 rows.push(["Concessionnaire", `${v.dealer_name || v.source}${v.city ? ` · ${v.city}` : ""}`]);3940 const texte = (v.description || "").trim();41 const long = texte.length > 420;42 const feats = v.features ?? [];43 const LIMIT = 12;44 const vin = v.vin_info && Object.keys(v.vin_info).length > 0 ? Object.entries(v.vin_info) : [];4546 return (47 <SectionCard id="vehicule" title="Le véhicule">48 <div className="ak-facts" role="list">49 {facts.map((x) => (50 <div className="ak-fact" role="listitem" key={x.l}>51 <span className="ak-fact-ico" aria-hidden="true">{x.ico}</span>52 <span className="ak-fact-txt"><div className="ak-fact-v" title={x.v}>{x.v}</div><div className="ak-fact-l">{x.l}</div></span>53 </div>54 ))}55 </div>56 <div className="ak-facts-rows">57 {rows.map(([k, val]) => <div className="ak-kv" key={k}><span className="k">{k}</span><span className="v">{val}</span></div>)}58 </div>59 <h3 className="ak-card-sub ak-subtitle" id="description">Description du concessionnaire</h3>60 {texte ? (61 <>62 <div className={`ak-desc ${long && !descOpen ? "clamped" : ""}`}><div className="ak-desc-body"><p>{texte}</p></div></div>63 {long && <MoreButton onClick={() => setDescOpen(!descOpen)} expanded={descOpen}>{descOpen ? "Réduire" : "Lire la suite"}</MoreButton>}64 </>65 ) : <p className="ak-fine" style={{ marginTop: 0 }}>Le concessionnaire ne fournit pas de description pour cette annonce.</p>}66 {feats.length > 0 && (67 <>68 <h3 className="ak-card-sub ak-subtitle" id="equipements">Équipements <small>· {feats.length}</small></h3>69 <div className="ak-amen" role="list">70 {(allFeat ? feats : feats.slice(0, LIMIT)).map((f) => (71 <div className="ak-amen-it" role="listitem" key={f}><span className="ak-amen-ico" aria-hidden="true"><Ico name="check" size={14} /></span><span className="ak-amen-txt">{f}</span></div>72 ))}73 </div>74 {feats.length > LIMIT && <MoreButton onClick={() => setAllFeat(!allFeat)} expanded={allFeat}>{allFeat ? "Réduire" : `Voir les ${feats.length} équipements`}</MoreButton>}75 </>76 )}77 {vin.length > 0 && (78 <Accordion title="Fiche constructeur (décodée du NIV)" meta={`${vin.length} champs`}>79 <div className="ak-facts-rows" style={{ marginTop: 0 }}>80 {vin.map(([k, val]) => <div className="ak-kv" key={k}><span className="k">{k}</span><span className="v">{val}</span></div>)}81 </div>82 <p className="ak-fine">Décodé du NIV — base vPIC (NHTSA). Les données constructeur peuvent différer de la version canadienne.{NBSP}</p>83 </Accordion>84 )}85 </SectionCard>86 );87}88