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/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