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%
3.7 KB · 66 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/ScoreCard.tsx : carte « KA Score » — anneau 0-100 animé, libellé,5//   composantes en barres (prix vs marché, kilométrage vs âge, qualité de la6//   fiche, fraîcheur), méthodologie en accordéon. Le score est calculé côté7//   API (autoka/web.py _ka_score) ; la carte ne l'invente jamais.8// -----------------------------------------------------------------------------9import { useEffect, useState } from "react";10import { KaScore } from "../api";11import { Ico } from "../components/Icons";12import { Accordion, SectionCard } from "./ui";1314export const kaLabel = (s: number) => (s >= 85 ? "Exceptionnel" : s >= 70 ? "Excellent" : s >= 55 ? "Très bon" : s >= 40 ? "Moyen" : "Faible");1516export function ScoreRing({ value, size = 88 }: { value: number | null; size?: number }) {17  const [v, setV] = useState(0);18  useEffect(() => { const t = requestAnimationFrame(() => setV(value ?? 0)); return () => cancelAnimationFrame(t); }, [value]);19  const r = 40, c = 2 * Math.PI * r;20  return (21    <div className="ak-score-ring" style={{ width: size, height: size }} role="img" aria-label={value != null ? `KA Score ${value} sur 100` : "Score non calculable"}>22      <svg viewBox="0 0 100 100" style={{ width: size, height: size }} aria-hidden="true">23        <circle className="bg" cx="50" cy="50" r={r} />24        <circle className="arc" cx="50" cy="50" r={r} strokeDasharray={`${(c * Math.max(0, Math.min(100, v))) / 100} ${c}`} />25      </svg>26      <div className="ak-score-val"><div>{value != null ? value : "—"}<small>/ 100</small></div></div>27    </div>28  );29}3031export default function ScoreCard({ s, resume }: { s: KaScore; resume: string[] }) {32  const manque = s.parts.length < 4;33  return (34    <SectionCard id="score" title="KA Score" icon={<Ico name="sparkles" size={18} />}35                 sub={manque ? "Score partiel — la composante prix manque (pas assez de comparables)" : undefined}>36      <div className="ak-score">37        <ScoreRing value={s.overall} />38        <div>39          <div className="ak-score-lbl">{kaLabel(s.overall)}</div>40          {resume.length > 0 && <div className="ak-score-sub">{resume.join(" · ")}</div>}41          <div className="ak-score-parts">42            {s.parts.map((p) => <span className="ak-score-part" key={p.key}>{p.label} <b>{p.score}</b></span>)}43          </div>44        </div>45      </div>46      <div className="ak-rows" style={{ marginTop: 12 }}>47        {s.parts.map((p) => (48          <div className="ak-row" key={p.key}>49            <span className="ak-row-name">{p.label}<small>· poids {p.weight}{" "}%</small></span>50            <span className="ak-row-bar" aria-hidden="true"><i className={p.score >= 70 ? "good" : p.score >= 45 ? "warn" : "bad"} style={{ width: `${p.score}%` }} /></span>51            <span className="ak-row-val">{p.score}</span>52          </div>53        ))}54      </div>55      <Accordion title="Comment est calculé ce score ?" small>56        <p>57          Indice composite Auto-Ka (0-100) calculé côté serveur : prix face à la médiane des comparables, kilométrage58          selon l'âge (référence ≈ 20 000 km/an), qualité de la fiche — photos, description, NIV, équipements — et59          fraîcheur de l'annonce. Les poids affichés à côté de chaque composante sont renormalisés quand une composante60          manque (par exemple sans comparables pour le prix). Il ne remplace ni l'inspection mécanique ni l'historique du véhicule.61        </p>62      </Accordion>63    </SectionCard>64  );65}66