Aller au contenu

Page de succès

Quand l'étudiant valide la dernière étape DYNAMIC de son parcours, le PedagogiqueOrchestrator navigue automatiquement vers une URL de la forme /s/:token. Cette page — la page de succès — est entièrement hors de la navbar et du footer habituels de la boutique PerfShop : elle occupe tout l'écran avec un fond dégradé sombre, un grand affichage d'étoiles, les statistiques du parcours et un sélecteur de thème final. Cette page documente son rendu, ses appels API, et son rôle dans le cycle de vie d'un parcours.

URL et authentification par token

L'URL /s/:token utilise le token de session comme seule preuve d'authentification. Le token est un UUID v4 (non-devinable), présent uniquement :

  • dans le localStorage du navigateur de l'étudiant (clé ped_student_token)
  • dans l'URL de la page de succès (paramètre de route :token)
  • dans la base pedagogique_sessions (clé primaire token)

Cette approche a plusieurs propriétés utiles :

  • Pas besoin de session HTTP : l'étudiant peut partager l'URL à un camarade ou la rouvrir depuis un autre navigateur, et la page s'affiche si et seulement si le token existe et correspond à un parcours complété.
  • Expiration naturelle : quand le formateur clique sur Désactiver, clearAll() supprime toutes les sessions de la base — toutes les URLs /s/:token renvoient alors 404.
  • Contrôle fin : un étudiant ayant perdu son localStorage ne retrouve pas sa page de succès, ce qui est voulu — les scores sont transitoires et liés à la vie de la session formateur.

Flux de navigation

sequenceDiagram
    autonumber
    actor E as Étudiant
    participant ORC as PedagogiqueOrchestrator
    participant OVR as PedagogiqueOverlay
    participant HOK as usePedagogiqueState
    participant CTR as ChaosStudentController
    participant R as react-router
    participant SUC as PedagogiqueSucces

    E->>OVR: Valide la dernière étape
    OVR->>CTR: POST /pedagogique/validate
    CTR-->>OVR: {valid:true, completed:true, stars}
    OVR->>HOK: refresh()
    HOK->>CTR: GET /status
    CTR-->>HOK: pedagogique.completed=true
    HOK->>HOK: state.successUrl = "/s/<token>"
    ORC->>ORC: useEffect détecte completed + successUrl
    ORC->>R: navigate("/s/<token>")
    R->>SUC: mount PedagogiqueSucces
    SUC->>CTR: GET /pedagogique/succes/<token>
    CTR-->>SUC: {alias, level, stars, duration, ...}
    SUC->>SUC: render SuccesStars + ThemeCards

Points à retenir :

  • Le token n'est pas dans la réponse de /validate : il est déjà dans le localStorage et dans l'URL cible construite par le hook (state.successUrl = '/s/' + tok). L'overlay n'a donc pas besoin de connaître le token.
  • Un garde-fou navigatedRef empêche le PedagogiqueOrchestrator de naviguer plus d'une fois, même si plusieurs polls arrivent en rafale avec completed:true.
  • Si successUrl est absent (token perdu entre le join et la completion), l'orchestrator affiche un message discret « parcours terminé, rechargez la page » au lieu de laisser un écran blanc.

Endpoint GET /pedagogique/succes/{token}

@GetMapping("/pedagogique/succes/{token}")
public ResponseEntity<?> getSuccesPage(@PathVariable String token) {
    PedagogiqueSessionService.MutableSession session =
        pedagogiqueSessionService.getSession(token);
    if (session == null)
        return ResponseEntity.status(404).body(Map.of("error",
            i18n.t("student.pedagogique.error.session_not_found")));

    List<PedagogiqueEnigme.Enigme> enigmes = pedagogiqueLevel != null
        ? PedagogiqueEnigme.forLevel(pedagogiqueLevel) : List.of();
    boolean completed = session.completedAt > 0
        && session.step.get() >= enigmes.size();
    if (!completed)
        return ResponseEntity.status(404).body(Map.of("error",
            i18n.t("student.pedagogique.error.not_completed")));

    long durationSeconds = (session.completedAt - session.joinedAt) / 1000L;
    int  stars    = computeStars(pedagogiqueLevel,
                                 durationSeconds <= pedagogiqueTimer);
    int  maxStars = computeStars(pedagogiqueLevel, true);

    return ResponseEntity.ok(Map.of(
        "alias",           session.displayAlias(),
        "level",           pedagogiqueLevel != null ? pedagogiqueLevel : "",
        "stars",           stars,
        "maxStars",        maxStars,
        "durationSeconds", durationSeconds,
        "completedAt",     session.completedAt,
        "totalSteps",      enigmes.size()
    ));
}
Propriété Valeur
Méthode GET
URL /api/chaos/student/pedagogique/succes/{token}
Authentification Aucune (le token {token} est l'auth)
Réponse 200 Voir tableau ci-dessous
Réponse 404 Token inconnu, ou parcours non complété

Structure de la réponse

Champ Type Description
alias string Alias saisi au /join, ou Agent-XXXX si anonyme
level string Identifiant du niveau (bac1bac5)
stars int Étoiles obtenues (incluant le malus hors temps)
maxStars int Étoiles maximales pour ce niveau
durationSeconds long Temps effectif entre joinedAt et completedAt
completedAt long Timestamp Unix en millisecondes
totalSteps int Nombre total d'étapes du niveau

Pourquoi renvoyer totalSteps

Le frontend l'utilise pour afficher un récapitulatif « 20 étapes résolues en 42 min ». Comme level est présent, il pourrait le calculer côté client, mais ce serait dupliquer la logique du catalogue Java — le serveur reste source unique.

Composant PedagogiqueSucces.jsx

Le composant, monté sur la route /s/:token de react-router, orchestre trois états de rendu :

  1. Chargement — affiche un spinner pendant le fetch de /pedagogique/succes/{token}.
  2. Erreur — si le token est invalide (404), affiche un cadenas 🔒 et un lien vers /products.
  3. Succès sans thème — affiche les étoiles, les statistiques, et les cartes des cinq thèmes à choisir.
  4. Succès avec thème sélectionné — charge dynamiquement le composant Theme*.jsx correspondant et le rend à la place des cartes (gardant les étoiles visibles en haut).

Hiérarchie React

flowchart TD
    ROOT["PedagogiqueSucces"]
    SHELL["PageShell<br/>(dégradé sombre plein écran)"]
    STARS["SuccesStars<br/>(étoiles + métriques)"]
    SEP["separator"]
    CARDS["Grid 2×3<br/>ThemeCard × 5"]
    THEME["ThemeComponent<br/>(lazy-loaded)"]
    SPIN["Spinner"]

    ROOT --> SHELL
    SHELL --> STARS
    STARS -.-> SEP
    SEP -.-> CARDS
    SEP -.-> THEME
    SHELL -.-> SPIN

Le composant racine est contenu dans un PageShell qui impose un dégradé sombre plein écran (#0d0d20#0d1a2e), une carte centrée de 560 px de large avec bordure verte tenue (rgba(52,211,153,0.25)) et un blur de fond. Cette esthétique « terminal futuriste » marque visuellement la rupture avec le reste de la boutique — l'étudiant sait qu'il est « sorti » du tunnel classique.

Lazy-loading des thèmes

Les cinq modules de thème sont chargés à la demande via import() dynamique déclaré dans themes/index.js :

export const THEMES = {
  performance: { id: 'performance', load: () => import('./ThemePerformance.jsx'), ... },
  fonctionnel: { id: 'fonctionnel', load: () => import('./ThemeFonctionnel.jsx'), ... },
  metier:      { id: 'metier',      load: () => import('./ThemeMetier.jsx'),      ... },
  securite:    { id: 'securite',    load: () => import('./ThemeSecurite.jsx'),    ... },
  logique:     { id: 'logique',     load: () => import('./ThemeLogique.jsx'),     ... },
};

L'étudiant qui choisit Logique ne télécharge pas le JS des quatre autres thèmes. Cette séparation accélère la première peinture de la page et permet d'ajouter un 6e thème plus tard sans alourdir le bundle principal.

Changement de thème

Un bouton « Changer de thème » sous le composant chargé permet à l'étudiant de revenir à la grille de cartes sans perdre sa session. Cette option est utile si l'étudiant découvre qu'un thème ne l'intéresse pas ou qu'il bloque sur une question — il peut retenter avec un autre thème tant que la session existe en base.

Composant SuccesStars

Le bloc d'étoiles du haut est un sous-composant dédié qui reçoit directement les données de l'API :

<SuccesStars
  alias={data.alias}
  level={data.level}
  stars={data.stars}
  maxStars={data.maxStars}
  durationSeconds={data.durationSeconds}
  totalSteps={data.totalSteps}
/>

Il affiche :

  • L'alias de l'agent en grand (Agent-3F7E ou le nom saisi)
  • Le niveau atteint (bac3) formaté en libellé lisible
  • Une rangée d'étoiles : stars pleines (⭐) + (maxStars - stars) vides (☆)
  • La durée formatée (42 min 17 s)
  • Le ratio étapes résolues (20 / 20)

L'esthétique reprend la palette verte (#34d399) du mode « chaos » de la boutique, avec une ombre portée sur les étoiles pleines pour l'emphase.

Hub de mini-jeux

En bas de la page de succès, une fois qu'un thème final est validé, l'étudiant est redirigé vers un hub de mini-jeux pédagogiques déployé à part. La redirection est déclenchée par la réponse de POST /pedagogique/finale/validate qui contient un champ gameUrl. Le frontend effectue un window.location.href = gameUrl pour quitter l'application PerfShop principale.

La documentation technique du hub lui-même n'est pas exposée dans cette référence. L'URL cible est injectée côté backend via la variable d'environnement PUBLIC_GAMES_URL — un administrateur souhaitant désactiver l'accès au hub peut positionner cette variable sur une URL de fallback (page d'information, page d'erreur contrôlée, etc.).

Avant validation d'un thème

Tant que l'étudiant n'a pas validé de thème final, la page de succès affiche simplement le récapitulatif et les cinq cartes. La notion de hub n'apparaît pas dans l'UI — l'étudiant doit d'abord choisir un thème et résoudre son énigme.

Gestion des erreurs

Le composant gère trois cas d'erreur :

Cas Cause Affichage
error.unauthorized GET /succes/{token} retourne 404 Cadenas 🔒, message « Lien invalide », bouton retour catalogue
theme.loadError L'import() dynamique du Theme*.jsx échoue (réseau, cache) Message « Erreur de chargement » + bouton retour sélecteur de thème
Thème KO La validation finale est refusée (hash invalide) Feedback en rouge dans le composant du thème, l'étudiant peut réessayer

Le composant gèle le token dans une useRef au premier rendu : const tokenRef = useRef(tokenParam); const token = tokenRef.current;. Ce gel évite qu'un changement d'URL en cours de route (improbable mais possible sur navigation manuelle) ne provoque un nouveau fetch avec un token différent.

Clés i18n concernées

Clé Utilisation
succes.unauthorized Titre du cas erreur 404
succes.invalidLink Sous-titre du cas erreur 404
succes.backToCatalog Bouton retour
succes.finalChallenge Bandeau au-dessus des cartes de thème
succes.chooseTheme Titre de la grille de thèmes
succes.oneRiddleLeft Sous-titre « une dernière énigme »
succes.changeTheme Bouton « Changer de thème »
succes.back Bouton retour générique
succes.loadError Erreur de chargement du composant Theme
themes.{id}.label / themes.{id}.description Libellé et description de chaque carte

Toutes les clés sont définies dans frontend/src/i18n/fr.json et son équivalent en.json. Ajouter une langue à la page de succès consiste à cloner l'un des deux fichiers et à le traduire.


Pages suivantes : ← Indices · Thèmes finaux →