SPB Git

spb/auto-ka Public

Python 82.8% TypeScript 11.9% CSS 5.1%
7.4 KB · 211 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// 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