SPB Git

spb/forma-ka Public

Python 65.1% TypeScript 17.9% CSS 16.4% HTML 0.5%
3.1 KB · 75 lines tsx
Raw Blame History
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