// ----------------------------------------------------------------------------- // Forma-Ka — Agrégateur de formations (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // pages/Formation.tsx : fiche d'une formation — l'accent est mis sur les // DÉTAILS (le prix est souvent absent, p. ex. cours universitaires) : // description → objectifs → plan de cours → préalables → clientèle → // infos pratiques. Colonne droite : synthèse, dates offertes, CTA source, // formations similaires. CTA sticky en bas d'écran (mobile). // ----------------------------------------------------------------------------- import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { Formation, MODE_ICONS, fetchFormation, fetchSources, fmtDate, fmtHours, fmtPrice, registerSourceNames, sourceName, } from "../api"; const NBSP = " "; export default function FormationPage() { const { uid } = useParams<{ uid: string }>(); const [f, setF] = useState(null); const [error, setError] = useState(null); // re-rendu quand le registre des noms de sources est chargé const [, setSourcesReady] = useState(false); useEffect(() => { fetchSources() .then((r) => { registerSourceNames(r.sources); setSourcesReady(true); }) .catch(() => {}); if (!uid) return; fetchFormation(uid).then(setF).catch((e) => setError(String(e))); window.scrollTo(0, 0); }, [uid]); if (error) return (
⚠️

Formation introuvable

{error}

Retour aux formations
); if (!f) return (
); const updated = f.updated_at ? new Date(f.updated_at * 1000).toLocaleDateString("fr-CA", { day: "numeric", month: "long", year: "numeric" }) : null; // chips clés — tout ce qui décrit la formation d'un coup d'œil const chips: string[] = []; if (f.training_type) chips.push(f.training_type); if (f.mode) chips.push(`${MODE_ICONS[f.mode] ?? ""} ${f.mode}`.trim()); const dur = f.duration || fmtHours(f.duration_hours); if (dur) chips.push(dur); if (f.credits) chips.push(f.credits); if (f.level) chips.push(`Niveau ${f.level}`); if (f.language) chips.push(f.language === "fr" ? "Français" : f.language === "en" ? "Anglais" : f.language); if (f.city) chips.push(f.city); if (f.code) chips.push(f.code); const sessions = (f.sessions ?? []).filter(Boolean); const img = f.images && f.images.length > 0 ? f.images[0] : null; return (
{/* ------- colonne gauche (desktop) : les DÉTAILS de la formation ---- */}
{img && (
{f.title}
)}

Description

{f.description ? f.description.split(/\n{2,}/).map((p, i) => (

{p}

)) :

La source ne fournit pas de description pour cette formation.

}
{f.objectives.length > 0 && (

Objectifs d'apprentissage

{f.objectives.map((o) => ( ✓ {o} ))}
)} {f.program.length > 0 && (

Plan de la formation

    {f.program.map((p, i) => (
  1. {p}
  2. ))}
)} {(f.prerequisites || f.audience) && (

Admission

{f.prerequisites && (
Préalables
{f.prerequisites}
)} {f.audience && (
Clientèle visée
{f.audience}
)}
)}

Infos pratiques

Établissement
{sourceName(f.source)}
{f.code && (
Sigle
{f.code}
)} {f.credential && (
Reconnaissance
{f.credential}
)} {f.credits && (
Crédits / UEC
{f.credits}
)} {dur && (
Durée
{dur}
)} {f.instructor && (
Formateur·trice
{f.instructor}
)} {f.price_label && (
Prix affiché
{f.price_label}
)} {f.schedule_label && (
Horaire affiché
{f.schedule_label}
)} {updated && (
Synchronisé
{updated}
)}
{/* ------- colonne droite (desktop) : synthèse, dates, similaires ---- */}
{f.price === 0 || f.is_free ? ( "Gratuit" ) : f.price != null ? ( <>{fmtPrice(f.price, f.price_label)} ) : ( Prix non affiché par la source )}

{f.title}

{[f.category, f.city].filter(Boolean).join(" · ")}
{chips.map((c) => {c})}
Voir chez {sourceName(f.source)} ↗
{sessions.length > 0 && (

Dates offertes

{sessions.map((s) => ( 📅 {fmtDate(s) ?? s} ))}
)} {f.tags.length > 0 && (

Thèmes

{f.tags.map((t) => ( {t} ))}
)} {(f.similar ?? []).length > 0 && (

Formations similaires

    {(f.similar ?? []).map((s) => (
  • {s.title}
    {[s.training_type, s.mode, s.duration, s.price != null ? fmtPrice(s.price) : "", sourceName(s.source)].filter(Boolean).join(" · ")}
  • ))}
)}
{updated && <>Dernière synchronisation : {updated}. } Les prix, dates et détails sont ceux affichés par la source — chaque fiche renvoie à la page originale de la formation.
{/* CTA sticky mobile — toujours visible */}
{f.price === 0 || f.is_free ? "Gratuit" : f.price != null ? fmtPrice(f.price, f.price_label) : (f.credits || "Détails")} Voir chez {sourceName(f.source)} ↗
); }