🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
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>
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
# 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
|
||||
|
||||
1. **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
|
||||
2. **And** chaque choix est enregistré dans `choices` du store
|
||||
3. **And** l'ordre suggéré des zones varie selon le chemin choisi
|
||||
4. **And** les textes du narrateur s'adaptent au chemin (transitions contextuelles)
|
||||
5. **And** tous les chemins permettent de visiter tout le contenu
|
||||
6. **And** tous les chemins mènent au contact (pas de "mauvais" choix)
|
||||
7. **And** le `currentPath` du store reflète le chemin actuel
|
||||
8. **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
|
||||
|
||||
- [ ] **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
|
||||
|
||||
- [ ] **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
|
||||
|
||||
- [ ] **Task 4: Intégrer les choix après chaque zone** (AC: #8)
|
||||
- [ ] Composant `ZoneEndChoice.vue` affiché à la fin de chaque page de zone
|
||||
- [ ] Proposer les options de destination selon le chemin
|
||||
- [ ] Utiliser ChoiceCards pour la présentation
|
||||
|
||||
- [ ] **Task 5: Mettre à jour le store** (AC: #2, #7)
|
||||
- [ ] Ajouter `currentPath` computed au store
|
||||
- [ ] Ajouter `suggestedNextZone` computed
|
||||
- [ ] Méthode pour obtenir le choix à un point donné
|
||||
|
||||
- [ ] **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
|
||||
|
||||
- [ ] **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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```vue
|
||||
<!-- 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 |
|
||||
|
||||
### File List
|
||||
|
||||
Reference in New Issue
Block a user