# Story 2.8: Page Parcours - Timeline narrative

Status: ready-for-dev

## Story

As a visiteur,
I want découvrir le parcours professionnel du développeur sous forme de timeline,
so that je comprends son évolution et son expérience.

## Acceptance Criteria

1. **Given** le visiteur accède à `/parcours` (FR) ou `/en/journey` (EN) **When** la page se charge **Then** une timeline verticale affiche les étapes chronologiques du parcours
2. **And** chaque étape affiche : date, titre, description narrative traduite
3. **And** sur desktop : les étapes alternent gauche/droite pour un effet visuel dynamique
4. **And** sur mobile : les étapes sont linéaires (toutes du même côté)
5. **And** une animation d'apparition au scroll est présente (respectant `prefers-reduced-motion`)
6. **And** des icônes ou images illustrent les étapes clés
7. **And** le contenu est bilingue (FR/EN) et chargé depuis l'API ou fichiers i18n
8. **And** les meta tags SEO sont dynamiques pour cette page
9. **And** la police serif narrative est utilisée pour les descriptions

## Tasks / Subtasks

- [ ] **Task 1: Décider de la source de données** (AC: #7)
  - [ ] Option A : Fichiers i18n (données statiques)
  - [ ] Option B : Table BDD + API (données dynamiques)
  - [ ] Recommandation : Fichiers i18n (le parcours change rarement, pas besoin de CRUD)

- [ ] **Task 2: Créer les données du parcours dans i18n** (AC: #2, #7)
  - [ ] Ajouter les clés `journey.milestones` dans fr.json et en.json
  - [ ] Structure : date, title, description, icon
  - [ ] 5-8 étapes du parcours professionnel

- [ ] **Task 3: Créer le composant TimelineItem** (AC: #2, #6, #9)
  - [ ] Créer `frontend/app/components/feature/TimelineItem.vue`
  - [ ] Props : milestone (date, title, description, icon)
  - [ ] Afficher l'icône/image, la date, le titre et la description
  - [ ] Utiliser font-narrative pour la description

- [ ] **Task 4: Créer la page parcours.vue** (AC: #1, #3, #4)
  - [ ] Créer `frontend/app/pages/parcours.vue`
  - [ ] Charger les milestones depuis i18n
  - [ ] Layout timeline vertical avec ligne centrale
  - [ ] Desktop : alternance gauche/droite
  - [ ] Mobile : toutes les étapes à droite

- [ ] **Task 5: Implémenter l'animation au scroll** (AC: #5)
  - [ ] Utiliser IntersectionObserver pour détecter l'entrée dans le viewport
  - [ ] Animation fade-in + slide-up pour chaque étape
  - [ ] Respecter prefers-reduced-motion
  - [ ] Créer un composable `useIntersectionObserver()`

- [ ] **Task 6: Design de la timeline** (AC: #3, #4)
  - [ ] Ligne centrale verticale (sky-dark-100)
  - [ ] Points de connexion sur la ligne (circles sky-accent)
  - [ ] Cards avec flèche vers la ligne centrale
  - [ ] Responsive : adaptation mobile

- [ ] **Task 7: Meta tags SEO** (AC: #8)
  - [ ] Titre : "Mon Parcours | Skycel"
  - [ ] Description du parcours

- [ ] **Task 8: Tests et validation**
  - [ ] Tester en FR et EN
  - [ ] Valider l'alternance desktop
  - [ ] Vérifier le layout mobile
  - [ ] Tester l'animation au scroll
  - [ ] Valider prefers-reduced-motion

## Dev Notes

### Structure des données dans i18n

**fr.json :**
```json
{
  "journey": {
    "title": "Mon Parcours",
    "pageTitle": "Parcours | Skycel",
    "pageDescription": "Découvrez le parcours professionnel de Célian, de ses débuts à aujourd'hui.",
    "milestones": [
      {
        "date": "2018",
        "title": "Premiers pas en développement",
        "description": "Découverte du code à travers des projets personnels. HTML, CSS, JavaScript deviennent mes nouveaux compagnons de route. L'étincelle est là.",
        "icon": "🚀"
      },
      {
        "date": "2019",
        "title": "Formation intensive",
        "description": "Plongée dans le monde du développement web professionnel. Apprentissage de frameworks modernes, bonnes pratiques, et méthodologies agiles.",
        "icon": "📚"
      },
      {
        "date": "2020",
        "title": "Premiers clients",
        "description": "Lancement en freelance. Premiers projets concrets, premiers défis réels. Chaque client m'apprend quelque chose de nouveau.",
        "icon": "💼"
      },
      {
        "date": "2021",
        "title": "Spécialisation Vue.js & Laravel",
        "description": "Le duo qui change tout. Vue.js côté front, Laravel côté back. Une stack qui me permet de créer des expériences web complètes et performantes.",
        "icon": "⚡"
      },
      {
        "date": "2022",
        "title": "Création de la micro-entreprise",
        "description": "Officialisation de l'aventure entrepreneuriale. L'araignée devient la mascotte, le Bug devient le guide. L'identité Skycel prend forme.",
        "icon": "🕷️"
      },
      {
        "date": "2023-2024",
        "title": "Projets ambitieux",
        "description": "Des applications web complexes aux sites e-commerce, chaque projet repousse les limites. TypeScript, Nuxt 4, et une obsession pour la qualité.",
        "icon": "🎯"
      },
      {
        "date": "2025",
        "title": "Aujourd'hui",
        "description": "Ce portfolio que vous explorez. Une aventure en soi, qui reflète ma passion pour créer des expériences web mémorables. Et ce n'est que le début...",
        "icon": "✨"
      }
    ]
  }
}
```

**en.json :**
```json
{
  "journey": {
    "title": "My Journey",
    "pageTitle": "Journey | Skycel",
    "pageDescription": "Discover Célian's professional journey, from the beginning to today.",
    "milestones": [
      {
        "date": "2018",
        "title": "First steps in development",
        "description": "Discovering code through personal projects. HTML, CSS, JavaScript became my new travel companions. The spark was there.",
        "icon": "🚀"
      },
      {
        "date": "2019",
        "title": "Intensive training",
        "description": "Deep dive into professional web development. Learning modern frameworks, best practices, and agile methodologies.",
        "icon": "📚"
      },
      {
        "date": "2020",
        "title": "First clients",
        "description": "Starting as a freelancer. First real projects, first real challenges. Each client teaches me something new.",
        "icon": "💼"
      },
      {
        "date": "2021",
        "title": "Specialization in Vue.js & Laravel",
        "description": "The game-changing duo. Vue.js on the front, Laravel on the back. A stack that allows me to create complete, performant web experiences.",
        "icon": "⚡"
      },
      {
        "date": "2022",
        "title": "Creating the micro-enterprise",
        "description": "Making the entrepreneurial adventure official. The spider becomes the mascot, the Bug becomes the guide. The Skycel identity takes shape.",
        "icon": "🕷️"
      },
      {
        "date": "2023-2024",
        "title": "Ambitious projects",
        "description": "From complex web applications to e-commerce sites, each project pushes boundaries. TypeScript, Nuxt 4, and an obsession with quality.",
        "icon": "🎯"
      },
      {
        "date": "2025",
        "title": "Today",
        "description": "This portfolio you're exploring. An adventure in itself, reflecting my passion for creating memorable web experiences. And this is just the beginning...",
        "icon": "✨"
      }
    ]
  }
}
```

### Composable useIntersectionObserver

```typescript
// frontend/app/composables/useIntersectionObserver.ts
export interface UseIntersectionObserverOptions {
  threshold?: number
  rootMargin?: string
  once?: boolean
}

export function useIntersectionObserver(
  target: Ref<HTMLElement | null>,
  options: UseIntersectionObserverOptions = {}
) {
  const { threshold = 0.1, rootMargin = '0px', once = true } = options

  const isVisible = ref(false)

  let observer: IntersectionObserver | null = null

  onMounted(() => {
    if (!target.value) return

    observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            isVisible.value = true
            if (once && observer) {
              observer.unobserve(entry.target)
            }
          } else if (!once) {
            isVisible.value = false
          }
        })
      },
      { threshold, rootMargin }
    )

    observer.observe(target.value)
  })

  onUnmounted(() => {
    if (observer) {
      observer.disconnect()
    }
  })

  return { isVisible }
}
```

### Composant TimelineItem

```vue
<!-- frontend/app/components/feature/TimelineItem.vue -->
<script setup lang="ts">
interface Milestone {
  date: string
  title: string
  description: string
  icon: string
}

const props = defineProps<{
  milestone: Milestone
  index: number
  isLeft: boolean
}>()

const itemRef = ref<HTMLElement | null>(null)
const reducedMotion = useReducedMotion()

const { isVisible } = useIntersectionObserver(itemRef, {
  threshold: 0.2,
  rootMargin: '-50px',
})

// Animation désactivée si prefers-reduced-motion
const shouldAnimate = computed(() => !reducedMotion.value)
</script>

<template>
  <div
    ref="itemRef"
    class="timeline-item relative flex"
    :class="[
      isLeft ? 'md:flex-row-reverse' : 'md:flex-row',
      'flex-row'
    ]"
  >
    <!-- Contenu de l'étape -->
    <div
      class="timeline-content w-full md:w-1/2 px-4 md:px-8"
      :class="[
        shouldAnimate && isVisible ? 'animate-in' : '',
        shouldAnimate && !isVisible ? 'opacity-0 translate-y-4' : '',
        !shouldAnimate ? '' : ''
      ]"
    >
      <div
        class="relative bg-sky-dark-50 rounded-lg p-6 shadow-lg"
        :class="[
          isLeft ? 'md:mr-8' : 'md:ml-8',
          'ml-8'
        ]"
      >
        <!-- Flèche vers la ligne -->
        <div
          class="absolute top-6 w-4 h-4 bg-sky-dark-50 transform rotate-45"
          :class="[
            isLeft ? 'md:-right-2 md:left-auto -left-2' : 'md:-left-2 -left-2',
          ]"
        ></div>

        <!-- Icône -->
        <div class="text-4xl mb-3">
          {{ milestone.icon }}
        </div>

        <!-- Date -->
        <span class="inline-block px-3 py-1 bg-sky-accent/20 text-sky-accent text-sm font-ui font-medium rounded-full mb-3">
          {{ milestone.date }}
        </span>

        <!-- Titre -->
        <h3 class="text-xl font-ui font-bold text-sky-text mb-2">
          {{ milestone.title }}
        </h3>

        <!-- Description -->
        <p class="font-narrative text-sky-text-muted leading-relaxed">
          {{ milestone.description }}
        </p>
      </div>
    </div>

    <!-- Point sur la ligne (visible uniquement côté desktop) -->
    <div class="timeline-dot absolute left-0 md:left-1/2 top-6 transform md:-translate-x-1/2 -translate-x-1/2">
      <div class="w-4 h-4 bg-sky-accent rounded-full ring-4 ring-sky-dark"></div>
    </div>
  </div>
</template>

<style scoped>
.animate-in {
  animation: fadeSlideUp 0.6s ease-out forwards;
}

@keyframes fadeSlideUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .timeline-content {
    opacity: 1 !important;
    transform: none !important;
  }

  .animate-in {
    animation: none;
  }
}
</style>
```

### Page parcours.vue

```vue
<!-- frontend/app/pages/parcours.vue -->
<script setup lang="ts">
const { t, tm } = useI18n()

interface Milestone {
  date: string
  title: string
  description: string
  icon: string
}

// Charger les milestones depuis i18n
const milestones = computed(() => {
  const data = tm('journey.milestones')
  if (Array.isArray(data)) {
    return data as Milestone[]
  }
  return []
})

// SEO
useHead({
  title: () => t('journey.pageTitle'),
})

useSeoMeta({
  title: () => t('journey.pageTitle'),
  description: () => t('journey.pageDescription'),
  ogTitle: () => t('journey.pageTitle'),
  ogDescription: () => t('journey.pageDescription'),
})
</script>

<template>
  <div class="container mx-auto px-4 py-8">
    <h1 class="text-3xl font-ui font-bold text-sky-text mb-12 text-center">
      {{ t('journey.title') }}
    </h1>

    <!-- Timeline -->
    <div class="relative">
      <!-- Ligne centrale (visible uniquement sur desktop) -->
      <div class="absolute left-0 md:left-1/2 top-0 bottom-0 w-0.5 bg-sky-dark-100 transform md:-translate-x-1/2"></div>

      <!-- Étapes -->
      <div class="space-y-12">
        <TimelineItem
          v-for="(milestone, index) in milestones"
          :key="index"
          :milestone="milestone"
          :index="index"
          :is-left="index % 2 === 0"
        />
      </div>
    </div>

    <!-- Message de fin -->
    <div class="mt-16 text-center">
      <p class="font-narrative text-xl text-sky-text-muted italic">
        {{ t('journey.endMessage') }}
      </p>
    </div>
  </div>
</template>
```

### Clés i18n supplémentaires

**fr.json :**
```json
{
  "journey": {
    "endMessage": "L'aventure continue... Qui sait où le code me mènera demain ?"
  }
}
```

**en.json :**
```json
{
  "journey": {
    "endMessage": "The adventure continues... Who knows where code will take me tomorrow?"
  }
}
```

### Design de la timeline

```
DESKTOP (alternance gauche/droite) :

    ┌─────────────────┐
    │   2018          │
    │   Description   │──●──
    └─────────────────┘  │
                         │
                  ──●────┼────┌─────────────────┐
                         │    │   2019          │
                         │    │   Description   │
                         │    └─────────────────┘
                         │
    ┌─────────────────┐  │
    │   2020          │──●──
    │   Description   │  │
    └─────────────────┘  │

MOBILE (linéaire à droite) :

    │  ┌─────────────────┐
    ●──│   2018          │
    │  │   Description   │
    │  └─────────────────┘
    │
    │  ┌─────────────────┐
    ●──│   2019          │
    │  │   Description   │
    │  └─────────────────┘
```

### Dépendances

**Cette story nécessite :**
- Story 1.3 : Système i18n configuré
- Story 1.4 : Layouts et routing

**Cette story prépare pour :**
- Aucune dépendance directe (dernière story de l'Epic 2)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── pages/
│   └── parcours.vue                 # CRÉER
├── components/feature/
│   └── TimelineItem.vue             # CRÉER
└── composables/
    └── useIntersectionObserver.ts   # CRÉER
```

**Fichiers à modifier :**
```
frontend/i18n/fr.json                # AJOUTER journey.*
frontend/i18n/en.json                # AJOUTER journey.*
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-2.8]
- [Source: docs/planning-artifacts/ux-design-specification.md#Screen-Architecture-Summary]
- [Source: docs/planning-artifacts/ux-design-specification.md#Typography-System]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Source données | Fichiers i18n | Décision technique |
| Layout desktop | Alternance gauche/droite | Epics |
| Layout mobile | Linéaire à droite | Epics |
| Animation | IntersectionObserver + fade-in | Epics |
| Police | font-narrative pour descriptions | UX Spec |

## 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

