spb/forma-ka Public
Python 65.1%
TypeScript 17.9%
CSS 16.4%
HTML 0.5%
1// -----------------------------------------------------------------------------2// Forma-Ka — Agrégateur de formations (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// components/FormationCard.tsx : carte de formation (grille de résultats)5// Pas d'image imposée (rare dans le domaine) : monogramme de la source +6// type de formation en badge, détails clés (mode, durée, ville, date).7// -----------------------------------------------------------------------------8import { Link } from "react-router-dom";9import { Formation, MODE_ICONS, fmtDate, fmtHours, fmtPrice, sourceName } from "../api";1011/** Couleur stable dérivée du domaine (pour le bandeau du monogramme). */12const HUES = [14, 36, 88, 152, 196, 226, 262, 318];13function hue(s: string): number {14 let h = 0;15 for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;16 return HUES[Math.abs(h) % HUES.length];17}1819export default function FormationCard({ f }: { f: Formation }) {20 const img = f.images && f.images.length > 0 ? f.images[0] : null;21 const start = fmtDate(f.start_date);22 const dur = f.duration || fmtHours(f.duration_hours);23 const h = hue(f.category || f.source);24 return (25 <Link to={`/formation/${encodeURIComponent(f.uid)}`} className="card">26 <div className="card-img" style={{ aspectRatio: "5 / 2" }}>27 {img ? (28 <img src={img} alt={f.title} loading="lazy" />29 ) : (30 <div31 className="noimg"32 style={{33 background: `linear-gradient(135deg, hsl(${h} 42% 90%), hsl(${h} 38% 78%))`,34 color: `hsl(${h} 45% 26%)`,35 fontFamily: "var(--font-display)",36 fontSize: "1rem", fontWeight: 700, padding: "0 16px",37 textAlign: "center", lineHeight: 1.25,38 }}39 >40 {f.category || sourceName(f.source)}41 </div>42 )}43 {f.training_type && <span className="badge type">{f.training_type}</span>}44 {f.is_free && <span className="badge right">Gratuit</span>}45 </div>46 <div className="card-body">47 <div className="card-price">48 {f.price != null && f.price > 0 ? (49 <>{fmtPrice(f.price, f.price_label)}</>50 ) : f.is_free ? (51 <>Gratuit</>52 ) : (53 <span style={{ color: "var(--ink-3)", fontSize: ".85em" }}>54 {f.credits || f.price_label || "Prix non affiché"}55 </span>56 )}57 {f.code && <small style={{ marginLeft: 8 }}>{f.code}</small>}58 </div>59 <div className="card-title">{f.title}</div>60 <div className="card-meta">61 {f.mode && <span>{MODE_ICONS[f.mode] ?? ""} {f.mode}</span>}62 {f.mode && dur && <span className="sep" />}63 {dur && <span>{dur}</span>}64 {(f.mode || dur) && f.city && <span className="sep" />}65 {f.city && <span>{f.city}</span>}66 </div>67 <div className="card-foot">68 <span className="source-tag">{sourceName(f.source)}</span>69 {start && <span className="avail">Débute le {start}</span>}70 </div>71 </div>72 </Link>73 );74}75