15 KiB
15 KiB
GuidedPath — Implémentation visitapp-web
Suivi d'implémentation du parcours guidé pour visitapp-web. Chaque phase peut être interrompue/reprise indépendamment.
✅ À tester (checklist complète)
Pré-requis
- Avoir au moins une
SectionMapavec quelques GeoPoints + au moins un parcours (idéalement plusieurs étapes) - Avoir au moins un
SectionGamede typeEscapeavec un parcours - Tester depuis un device avec GPS (mobile ou navigateur desktop avec localisation activée)
- Tester en HTTPS (la Geolocation API est bloquée en HTTP sauf sur
localhost)
Mode Carte (SectionMap)
Affichage de base
- Sur une SectionMap sans parcours : aucun bouton "Parcours" visible
- Sur une SectionMap avec parcours : bouton "Parcours (N)" dans la top bar
- Tap sur le bouton → bottom sheet liste les parcours avec titre + nb étapes + badge "linéaire" si applicable
Sélection d'un parcours
- Tap sur un parcours dans la liste → la carte se recadre (flyToBounds) sur les étapes
- Polyline pointillée colorée relie les étapes dans l'ordre
- Markers étapes numérotés (1, 2, 3…) visibles
- POI passent en opacity réduite (0.45)
- Bouton change en "Étape 1/N"
- Bouton "Quitter le parcours" apparaît en bottom-left
États visuels des markers étapes
- Étape courante : couleur primaire avec animation pulse
- Étape complétée : vert avec ✓
- Étape future : gris clair
- Étape avec
isStepLocked: gris avec 🔒 - Tap sur un marker step → bottom sheet de détail s'ouvre
Marqueur utilisateur GPS
- Premier accès au parcours → demande de permission GPS du navigateur
- Permission accordée → point bleu pulsant apparaît à votre position
- Le point se déplace en live si vous bougez
StepDetail (bottom sheet d'une étape)
- Header avec image de l'étape + badge "Étape N/M"
- Titre + description (HTML rendu)
- Si
zoneRadiusMeters > 0: badge "À X m de la zone" qui passe vert "Vous êtes dans la zone" quand vous approchez - Si GPS refusé : bouton "Localisation refusée — réessayer" (clic → re-demande la permission)
- Boutons Précédent/Suivant uniquement en mode non-linéaire
Validation et progression
- Bouton "Marquer terminée" sur l'étape courante
- Si
requireSuccessToAdvance+ zone non atteinte → bouton désactivé - Validation → étape passe en complétée + ouverture auto de la suivante
- Dernière étape validée → modal "Bravo !" avec "Recommencer" / "Retour à la carte"
- Étape déjà complétée → badge "✓ Étape terminée" affiché à la place du bouton
Quiz par étape
- Si
step.quizQuestionsnon vide → bloc "Défi" avec QCM - Sélection des réponses → bouton "Valider mes réponses" s'active quand toutes répondues
- Validation → feedback vert (correct) / rouge (mauvaise sélection) sur les choix
- Si tout correct (100%) → badge "✓ Défi réussi"
- Si pas 100% → bouton "Réessayer" (réinitialise le quiz)
- Si
requireSuccessToAdvanceet quiz non passé → bouton "Marquer terminée" désactivé
Timer par étape
- Si
step.isStepTimer && step.timerSeconds > 0→ barre décompte affichée - Couleur barre : vert > 50%, orange 25-50%, rouge < 25%
- Format temps : MM:SS
- À expiration → message
timerExpiredMessageaffiché en rouge (si défini)
Toast d'entrée en zone
- Première fois que vous entrez dans la zone de l'étape courante → toast vert "Zone atteinte : « titre »"
- Le toast disparaît après ~3.5s
- Si vous quittez puis revenez dans la zone : pas de re-déclenchement (1× par étape)
Modes de parcours
isLinear = true: pas de boutons Précédent/Suivant, progression strictement par validationisLinear = false: navigation libre Précédent/Suivant entre étapeshideNextStepsUntilComplete = true: seules les étapes complétées + courante visibles sur la carteisHiddenInitiallysur une étape : invisible tant qu'elle n'est pas devenue courante ou complétée
Lock
isStepLocked+requireSuccessToAdvance→ message "🔒 Étape verrouillée — validez la condition"- Validation (quiz passé OU dans la zone) → message "🔒 Étape déverrouillée"
Mode Game Escape (SectionGame type Escape)
- Sur un Game type Escape sans parcours : stub "Parcours guidé bientôt disponible"
- Avec parcours :
EscapeProgressionrendu (vue verticale, pas de carte) - Si plusieurs parcours : sélecteur initial avec liste de cartes
- Si un seul parcours : auto-sélectionné
- Compteur "N/M étapes" affiché en haut + bouton "Changer de parcours" (si plusieurs)
- Étapes empilées verticalement (carte par étape)
- Étape courante : bordure couleur primaire, étapes complétées vertes, futures grisées (opacity 0.6)
- Quiz / Timer / Toast / GPS retry / lock fonctionnent identiquement au mode carte
- Modal de fin avec "Recommencer" / "Retour"
Bugs connus à valider
- Endpoint Game Escape : on utilise
/api/SectionMap/{id}/GuidedPathpour les SectionGame aussi (hypothèse polymorphe). Si ça renvoie 404, le backend doit exposer/api/SectionGame/{id}/GuidedPathou inclureguidedPathsdans le DTO/api/Section/configuration/{id}/detail
Contexte
- Pas un type de Section standalone : GuidedPath est une entité attachée à
SectionMap,SectionGame(type Escape), ouSectionEvent. - Endpoint backend :
GET /api/SectionMap/{sectionMapId}/GuidedPathretourne les parcours avecsteps + quizQuestions + triggerGeoPointeager-loadés. - Référence Flutter :
mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_map_progression_page.dart(mode carte, ~770 lignes)mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_content_progression_page.dart(mode contenu, ~450 lignes)mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_list_sheet.dart(sélecteur)mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_step_timer.dart(widget timer)
Phase 1 — Découverte et affichage statique sur la carte
Objectif : permettre au visiteur de voir qu'il y a des parcours, choisir un parcours, et voir ses étapes tracées sur la carte (sans GPS, sans quiz, sans validation).
Fichiers touchés
src/lib/api/types.ts: ajoutGuidedPathDTO,GuidedStepDTO, champguidedPaths?surMapDTOsrc/lib/api/client.ts: nouvelle fonctiongetGuidedPaths(sectionMapId, apiKey)src/components/sections/MapSection.tsx:- fetch des parcours au mount (si
map.pointsexiste) - bouton flottant "Parcours" en haut-droite (si parcours dispo)
- bottom sheet listant les parcours (titre + nb étapes)
- état
selectedPathIdqui filtre l'affichage carte
- fetch des parcours au mount (si
src/components/sections/map/LeafletMap.tsx:- prop optionnelle
pathSteps?: { lat, lng, order, title }[] - rendu polyline reliant les étapes
- markers étapes numérotés (style différent des points POI)
- prop optionnelle
États UX à couvrir
- Aucun parcours dispo → bouton caché
- Parcours dispo, aucun sélectionné → bouton "Parcours (N)" + ouverture liste au tap
- Parcours sélectionné → polyline + steps numérotés sur la carte + bouton "Quitter le parcours"
- Tap sur step → bottom sheet de l'étape (titre, image, description, badge ordre)
Hors scope Phase 1
- GPS tracking
- Quiz par étape
- Timer
- Lock / progression réelle
- Page de progression dédiée (full-screen)
État
- Types (
GuidedPathDTO,GuidedStepDTO, champguidedPathssurMapDTO) - API client
getGuidedPaths(sectionMapId) - Pré-fetch server-side dans
app/[slug]/[configId]/sections/[sectionId]/page.tsx(les paths sont injectés danssection.map.guidedPathsavant le render client) - Bouton "Parcours" dans la top bar de
MapSection(visible si paths > 0, change en "Étape N/M" quand parcours actif) - Bottom sheet
PathListSheetlistant les parcours (titre + nb étapes + flag linéaire) - Polyline pointillée + step markers numérotés stylés (CSS pulse sur sélection)
- Bottom sheet
StepDetail(image header + titre + description + badge zone + nav précédent/suivant) - Bouton "Quitter le parcours" en bottom-left quand un parcours est actif
- Auto-fit des bounds de la carte sur les étapes du parcours sélectionné
- POI dim (opacity 0.45) quand un parcours est actif pour mettre les étapes en avant
Phase 2 — Progression complète (en cours)
Objectif : reproduire GuidedPathMapProgressionPage du Flutter.
Fait
src/lib/geo.ts:haversineMeters()+formatDistance()src/hooks/useGeolocation.ts: hookwatchPositionavec étatsidle | requesting | granted | denied | unavailable- Hook activé seulement quand un parcours est actif (économie de batterie)
- StepDetail affiche en live :
- Distance jusqu'à la zone (formatée m/km)
- Badge vert "Vous êtes dans la zone" si
distance <= zoneRadiusMeters - États dégradés : "Localisation en cours…", "Localisation refusée", "GPS indisponible"
Fait (suite — 2e itération)
- Marqueurs étapes différenciés dans
LeafletMap:- Étape courante : couleur primaire pulsante (animation
mim-step-current), z-index 2000 - Étape complétée : vert (#16a34a) avec ✓
- Étape locked : gris avec 🔒
- Étape future : gris clair
- Étape courante : couleur primaire pulsante (animation
- Marqueur utilisateur : point bleu pulsant (
mim-user), position =geo.lat/lng, z-index 3000, non-interactif - Progression in-memory dans
MapSection:completedSteps: Set<string>reset à chaque changement de parcourscurrentStepId= première étape non complétée dans l'ordremarkStepComplete(): ajoute aux completed + avance auto vers la suivante (ou ouvre la modal de fin si dernière)
hideNextStepsUntilCompletegéré :visibleStepsslice àcurrentStep + 1, polyline et markers respectent- Conditions
canAdvancedansStepDetail:!requireSuccess || radius === 0 || inZone || geo refusé/indispo- Bouton "Marquer terminée" / "Terminer le parcours" disabled sinon, avec tooltip explicatif
- Mode non-linéaire : nav Précédent/Suivant n'apparaît que si
!path.isLinear. En linéaire, l'avancement est strictement par validation - Badge "✓ Étape terminée" affiché si l'étape consultée est déjà complétée
- Modal de fin
EndOfPathModal: bravo + boutons "Retour à la carte" / "Recommencer"
Fait (suite — 3e itération, finale Phase 2)
StepTimer(map/StepTimer.tsx) : barre décroissante + MM:SS + couleurs vert/orange/rouge (≥50/≥25/<25%), callbackonExpiredStepQuiz(map/StepQuiz.tsx) : QCM par question (réponses ordonnées), bouton Valider si toutes répondues, feedback vert/rouge sur les bonnes/mauvaises réponses après validation, bouton Réessayer, callback(passed, score). Passe = 100%Toast(map/Toast.tsx) : toast vert qui apparaît en haut, auto-dismiss 3.5s, animation slide-in- Quiz / Timer / Toast intégrés dans
StepDetail:- Quiz affiché si
step.quizQuestionsnon vide ET étape courante non complétée - Timer affiché si
step.isStepTimer && step.timerSeconds > 0ET étape courante - Toast déclenché à la première entrée dans la zone d'une étape (transition false→true)
- Quiz affiché si
- Conditions
canAdvancecomposées :quizOk = !hasQuiz || quizPassed || !requireSuccess zoneOk = radius === 0 || inZone || !requireSuccess || geoUnavailable lockOk = !isLocked || !requireSuccess || quizPassed || inZone || geoUnavailable canAdvance = quizOk && zoneOk && lockOk - Lock dur :
isStepLockedimpose au moins une preuve (quiz passé OU dans la zone) pour valider quandrequireSuccess isHiddenInitially: filtré dansvisibleSteps— uniquement affichées si complétée ou devenue courante- Retry permission GPS : bouton "Localisation refusée — réessayer" dans le StepDetail, qui incrémente
geoEnabledKey→ re-mount du watcher - Hook
useGeolocationétendu pour accepter unrefreshKeyqui force le re-watch
Phase 2.bis — Game Escape (FAIT)
GameDTO.guidedPathsajouté aux typesgetGuidedPathsForGame()dans le client API (utilise le même endpoint/api/SectionMap/{id}/GuidedPathcar la tableGuidedPathest polymorphe via parent ID — ajustable si le backend expose un endpoint dédié)- Server component pré-fetch les paths pour les sections Game de type Escape
game/EscapeProgression.tsx— vue verticale sans carte, avec :- Sélecteur de parcours si plusieurs (auto-sélection si un seul)
- Liste verticale des étapes (cartes empilées)
- Étape courante mise en valeur avec bordure primaire, étapes complétées en vert, étapes futures grisées
- Réutilise
StepTimer,StepQuiz,Toast,MessageDialog - Compteur "N/M étapes" en haut + bouton "Changer de parcours"
- Modal de fin avec Recommencer / Retour
GameSectionbrancheEscapeProgressionquandgameType === 'Escape'ETguidedPathsnon vide, sinon fallbackEscapeStub
État global
Tout le périmètre GuidedPath spécifié est implémenté côté visitapp-web :
- Mode carte (Map section) : sélection parcours, polyline + steps numérotés, marqueurs différenciés (courant/complété/locked/future), marqueur user GPS, progression in-memory, conditions composées, quiz/timer/lock/toast/retry GPS, modal de fin
- Mode contenu (Game type Escape) : même logique sans la carte, vue empilée verticale
Points d'attention pour validation backend :
- L'endpoint
/api/SectionMap/{id}/GuidedPathest utilisé pour Map ET Game Escape (hypothèse : table polymorphe via parent ID). À tester avec une section Game Escape réelle ; sinon le backend doit exposer/api/SectionGame/{id}/GuidedPathou inclureguidedPathsdirectement dans le DTO retourné par/api/Section/configuration/{id}/detail - Modal de fin de parcours : à la dernière étape validée, afficher un dialog "Bravo !" + bouton retour
- Permission GPS : si
geo.status === 'denied', afficher un overlay explicatif avec bouton "Réessayer" qui re-mount le hook (nécessite unkeyou unsetEnabled(false); setEnabled(true))
Phase 2.bis — Mode Game Escape
- Étendre
GameDTOweb avecguidedPaths?: GuidedPathDTO[] - Endpoint backend équivalent : à vérifier (probablement
GET /api/SectionGame/{id}/GuidedPathou inclus dans le DTO) - Page
GuidedPathContentProgressionPage(sans carte, vue verticale) - Brancher depuis
GameSectionquandgameType === 'Escape'(remplacer le stub actuelEscapeStub)
Phase 2.bis — Mode Game Escape
- Page
GuidedPathContentProgressionPage(sans carte, vue verticale) - Brancher depuis
GameSectionquandgameType === 'Escape' - Nécessite ajout
guidedPaths?surGameDTO