Setup complet de l'infrastructure projet : - Frontend Nuxt 4 (SSR, TypeScript, i18n, Pinia, TailwindCSS) - Backend Laravel 12 API-only avec middleware X-API-Key et CORS - Design tokens (sky-dark, sky-accent, sky-text) et polices (Merriweather, Inter) - Documentation planning et implementation artifacts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
14 KiB
Story 4.3: Chemins narratifs différenciés
Status: ready-for-dev
Story
As a visiteur, I want que mes choix aient un impact visible sur mon parcours, so that je sens que mon expérience est vraiment personnalisée.
Acceptance Criteria
- Given le visiteur fait des choix tout au long de l'aventure When il navigue entre les zones Then 2-3 points de choix binaires créent 4-8 parcours possibles
- And chaque choix est enregistré dans
choicesdu store - And l'ordre suggéré des zones varie selon le chemin choisi
- And les textes du narrateur s'adaptent au chemin (transitions contextuelles)
- And tous les chemins permettent de visiter tout le contenu
- And tous les chemins mènent au contact (pas de "mauvais" choix)
- And le
currentPathdu store reflète le chemin actuel - And à la fin de chaque zone, le narrateur propose un choix vers la suite
Tasks / Subtasks
-
Task 1: Définir l'arbre des chemins (AC: #1, #5, #6)
- Créer
frontend/app/data/narrativePaths.ts - Définir 2-3 points de choix créant 4-8 parcours
- S'assurer que tous les chemins visitent toutes les zones
- S'assurer que tous les chemins mènent au contact
- Créer
-
Task 2: Créer le composable useNarrativePath (AC: #2, #3, #7)
- Créer
frontend/app/composables/useNarrativePath.ts - Calculer le chemin actuel basé sur les choix
- Exposer la prochaine zone suggérée
- Exposer les zones restantes dans l'ordre
- Créer
-
Task 3: Ajouter les textes de transition contextuels (AC: #4)
- Créer des contextes spécifiques :
transition_after_projects_to_skills, etc. - Variantes selon le chemin pris
- Commentaires du narrateur sur les choix précédents
- Créer des contextes spécifiques :
-
Task 4: Intégrer les choix après chaque zone (AC: #8)
- Composant
ZoneEndChoice.vueaffiché à la fin de chaque page de zone - Proposer les options de destination selon le chemin
- Utiliser ChoiceCards pour la présentation
- Composant
-
Task 5: Mettre à jour le store (AC: #2, #7)
- Ajouter
currentPathcomputed au store - Ajouter
suggestedNextZonecomputed - Méthode pour obtenir le choix à un point donné
- Ajouter
-
Task 6: Créer l'API pour les transitions contextuelles (AC: #4)
- Endpoint
/api/narrator/transition-contextual - Paramètres : from_zone, to_zone, path_choices
- Retourner un texte adapté au contexte
- Endpoint
-
Task 7: Tests et validation
- Tester tous les chemins possibles (4-8)
- Vérifier que tous mènent au contact
- Valider les textes contextuels
- Tester la suggestion de zone suivante
Dev Notes
Arbre des chemins narratifs
// frontend/app/data/narrativePaths.ts
// Points de choix dans l'aventure
export const NARRATIVE_CHOICE_POINTS = {
// Point 1 : Après l'intro
intro: {
id: 'intro',
options: ['projects', 'skills'],
},
// Point 2 : Après la première zone
after_first_zone: {
id: 'after_first_zone',
options: ['testimonials', 'journey'],
},
// Point 3 : Après la deuxième zone
after_second_zone: {
id: 'after_second_zone',
// Les options dépendent de ce qui reste
},
}
// Chemins possibles (4-8 combinaisons)
// Format : intro_choice -> after_first -> after_second -> contact
export const NARRATIVE_PATHS = [
// Chemin 1 : Projets → Témoignages → Compétences → Parcours → Contact
['projects', 'testimonials', 'skills', 'journey', 'contact'],
// Chemin 2 : Projets → Témoignages → Parcours → Compétences → Contact
['projects', 'testimonials', 'journey', 'skills', 'contact'],
// Chemin 3 : Projets → Parcours → Témoignages → Compétences → Contact
['projects', 'journey', 'testimonials', 'skills', 'contact'],
// Chemin 4 : Projets → Parcours → Compétences → Témoignages → Contact
['projects', 'journey', 'skills', 'testimonials', 'contact'],
// Chemin 5 : Compétences → Témoignages → Projets → Parcours → Contact
['skills', 'testimonials', 'projects', 'journey', 'contact'],
// Chemin 6 : Compétences → Témoignages → Parcours → Projets → Contact
['skills', 'testimonials', 'journey', 'projects', 'contact'],
// Chemin 7 : Compétences → Parcours → Témoignages → Projets → Contact
['skills', 'journey', 'testimonials', 'projects', 'contact'],
// Chemin 8 : Compétences → Parcours → Projets → Témoignages → Contact
['skills', 'journey', 'projects', 'testimonials', 'contact'],
]
// Mapper zone key -> route
export const ZONE_ROUTES: Record<string, { fr: string; en: string }> = {
projects: { fr: '/projets', en: '/en/projects' },
skills: { fr: '/competences', en: '/en/skills' },
testimonials: { fr: '/temoignages', en: '/en/testimonials' },
journey: { fr: '/parcours', en: '/en/journey' },
contact: { fr: '/contact', en: '/en/contact' },
}
Composable useNarrativePath
// frontend/app/composables/useNarrativePath.ts
import { NARRATIVE_PATHS, ZONE_ROUTES } from '~/data/narrativePaths'
export function useNarrativePath() {
const progressionStore = useProgressionStore()
const { locale } = useI18n()
// Déterminer le chemin actuel basé sur les choix
const currentPath = computed(() => {
const choices = progressionStore.choices
// Trouver le premier choix (intro)
const introChoice = choices.find(c => c.id === 'intro_first_choice')
if (!introChoice) return null
const startZone = introChoice.value === 'choice_projects_first' ? 'projects' : 'skills'
// Filtrer les chemins qui commencent par cette zone
let possiblePaths = NARRATIVE_PATHS.filter(path => path[0] === startZone)
// Affiner avec les choix suivants
const afterFirstChoice = choices.find(c => c.id === 'after_first_zone')
if (afterFirstChoice && possiblePaths.length > 1) {
const secondZone = afterFirstChoice.value.includes('testimonials') ? 'testimonials' : 'journey'
possiblePaths = possiblePaths.filter(path => path[1] === secondZone)
}
return possiblePaths[0] || null
})
// Zone actuelle basée sur la route
const currentZone = computed(() => {
const route = useRoute()
const path = route.path.toLowerCase()
for (const [zone, routes] of Object.entries(ZONE_ROUTES)) {
if (path.includes(routes.fr.slice(1)) || path.includes(routes.en.slice(4))) {
return zone
}
}
return null
})
// Index de la zone actuelle dans le chemin
const currentZoneIndex = computed(() => {
if (!currentPath.value || !currentZone.value) return -1
return currentPath.value.indexOf(currentZone.value)
})
// Prochaine zone suggérée
const suggestedNextZone = computed(() => {
if (!currentPath.value || currentZoneIndex.value === -1) return null
const nextIndex = currentZoneIndex.value + 1
if (nextIndex >= currentPath.value.length) return null
return currentPath.value[nextIndex]
})
// Zones restantes à visiter
const remainingZones = computed(() => {
if (!currentPath.value) return []
const visited = progressionStore.visitedSections
return currentPath.value.filter(zone =>
zone !== 'contact' && !visited.includes(zone as any)
)
})
// Obtenir la route pour une zone
function getZoneRoute(zone: string): string {
const routes = ZONE_ROUTES[zone]
if (!routes) return '/'
return locale.value === 'fr' ? routes.fr : routes.en
}
// Générer le choix pour après la zone actuelle
function getNextChoicePoint() {
if (!remainingZones.value.length) {
// Plus de zones, aller au contact
return {
id: 'go_to_contact',
choices: [
{
id: 'contact',
textFr: 'Rencontrer le développeur',
textEn: 'Meet the developer',
icon: '📧',
destination: getZoneRoute('contact'),
zoneColor: '#fa784f',
},
],
}
}
// Proposer les 2 prochaines zones
const nextTwo = remainingZones.value.slice(0, 2)
return {
id: `after_${currentZone.value}`,
questionFr: 'Où vas-tu ensuite ?',
questionEn: 'Where to next?',
choices: nextTwo.map(zone => ({
id: `choice_${zone}`,
textFr: getZoneLabel(zone, 'fr'),
textEn: getZoneLabel(zone, 'en'),
icon: getZoneIcon(zone),
destination: getZoneRoute(zone),
zoneColor: getZoneColor(zone),
})),
}
}
return {
currentPath,
currentZone,
suggestedNextZone,
remainingZones,
getZoneRoute,
getNextChoicePoint,
}
}
// Helpers
function getZoneLabel(zone: string, locale: string): string {
const labels: Record<string, { fr: string; en: string }> = {
projects: { fr: 'Découvrir les créations', en: 'Discover the creations' },
skills: { fr: 'Explorer les compétences', en: 'Explore the skills' },
testimonials: { fr: 'Écouter les témoignages', en: 'Listen to testimonials' },
journey: { fr: 'Suivre le parcours', en: 'Follow the journey' },
}
return labels[zone]?.[locale] || zone
}
function getZoneIcon(zone: string): string {
const icons: Record<string, string> = {
projects: '💻',
skills: '⚡',
testimonials: '💬',
journey: '📍',
}
return icons[zone] || '?'
}
function getZoneColor(zone: string): string {
const colors: Record<string, string> = {
projects: '#3b82f6',
skills: '#10b981',
testimonials: '#f59e0b',
journey: '#8b5cf6',
}
return colors[zone] || '#fa784f'
}
Composant ZoneEndChoice
<!-- frontend/app/components/feature/ZoneEndChoice.vue -->
<script setup lang="ts">
const { getNextChoicePoint, remainingZones } = useNarrativePath()
const narrator = useNarrator()
const choicePoint = computed(() => getNextChoicePoint())
// Afficher un message du narrateur avant le choix
onMounted(async () => {
if (remainingZones.value.length > 0) {
await narrator.showTransitionChoice()
} else {
await narrator.showContactReady()
}
})
</script>
<template>
<div class="zone-end-choice py-16 px-4 border-t border-sky-dark-100 mt-16">
<div class="max-w-2xl mx-auto">
<!-- Message narratif -->
<p class="font-narrative text-xl text-sky-text text-center mb-8 italic">
{{ $t('narrative.whatNext') }}
</p>
<!-- Choix -->
<ChoiceCards
v-if="choicePoint.choices?.length"
:choice-point="choicePoint"
/>
<!-- Si une seule option (contact) -->
<div
v-else-if="choicePoint.choices?.length === 1"
class="text-center"
>
<NuxtLink
:to="choicePoint.choices[0].destination"
class="inline-flex items-center gap-3 px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-xl hover:bg-sky-accent/90 transition-colors"
>
<span class="text-2xl">{{ choicePoint.choices[0].icon }}</span>
<span>{{ $i18n.locale === 'fr' ? choicePoint.choices[0].textFr : choicePoint.choices[0].textEn }}</span>
</NuxtLink>
</div>
</div>
</div>
</template>
Schéma des chemins narratifs
┌─────────────┐
│ INTRO │
└──────┬──────┘
│
┌────────────┴────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ PROJETS │ │ COMPÉT. │
└────┬─────┘ └────┬─────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│TÉMOIGN.│ │PARCOURS│ │TÉMOIGN.│ │PARCOURS│
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
▼ ▼ ▼ ▼
(suite) (suite) (suite) (suite)
│ │ │ │
└──────┬──────┴───────────┴──────┬──────┘
│ │
▼ ▼
┌──────────────────────────────────────┐
│ CONTACT │
│ (tous les chemins y mènent) │
└──────────────────────────────────────┘
Dépendances
Cette story nécessite :
- Story 4.1 : ChoiceCards
- Story 4.2 : Intro narrative (premier choix)
- Story 3.5 : Store de progression (choices)
Cette story prépare pour :
- Story 4.7 : Révélation (fin des chemins)
- Story 4.8 : Page contact (destination finale)
Project Structure Notes
Fichiers à créer :
frontend/app/
├── data/
│ └── narrativePaths.ts # CRÉER
├── composables/
│ └── useNarrativePath.ts # CRÉER
└── components/feature/
└── ZoneEndChoice.vue # CRÉER
Fichiers à modifier :
frontend/app/pages/projets.vue # AJOUTER ZoneEndChoice
frontend/app/pages/competences.vue # AJOUTER ZoneEndChoice
frontend/app/pages/temoignages.vue # AJOUTER ZoneEndChoice
frontend/app/pages/parcours.vue # AJOUTER ZoneEndChoice
References
- [Source: docs/planning-artifacts/epics.md#Story-4.3]
- [Source: docs/planning-artifacts/ux-design-specification.md#Narrative-Paths]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Parcours-Narratifs]
Technical Requirements
| Requirement | Value | Source |
|---|---|---|
| Points de choix | 2-3 | Epics |
| Parcours possibles | 4-8 | Epics |
| Toutes zones visitables | Oui | Epics |
| Tous chemins → contact | Oui | Epics |
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}
Debug Log References
Completion Notes List
Change Log
| Date | Change | Author |
|---|---|---|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |