SPB Git forge

spb/auto-ka

Public
61commits 1branches 0releases
14.4 MBsize
maindefault branch
13 days agolast push
Python 61.6% TypeScript 20.9% CSS 11.4% JavaScript 5.1% HTML 1.1%
5.4 KB · 88 lines tsx
Raw Blame History
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