# Story 1.4: Layouts, routing et transitions de page Status: ready-for-dev ## Story As a visiteur, I want une navigation fluide entre les pages avec des transitions immersives, so that l'expérience ressemble à un changement de zone, pas à un rechargement. ## Acceptance Criteria 1. **Given** la structure de pages Nuxt 4 (`app/pages/`) **When** le visiteur navigue entre les pages **Then** les transitions de page sont animées (fade + slide) via `pageTransition` dans `nuxt.config.ts` 2. **And** la navigation utilise `` pour l'hydration SPA (pas de rechargement) 3. **And** le layout par défaut (`default.vue`) inclut le header avec barre de progression (placeholder) et sélecteur de langue 4. **And** un layout `minimal.vue` existe pour le mode express 5. **And** le `scrollBehavior` est personnalisé (smooth scroll, retour position sauvegardée) 6. **And** `prefers-reduced-motion` désactive les animations de transition via media query CSS 7. **And** une page 404 (`error.vue`) bilingue est en place 8. **And** les meta tags SEO dynamiques fonctionnent via `useHead()` et `useSeoMeta()` 9. **And** le favicon est configuré ## Tasks / Subtasks - [ ] **Task 1: Structure des pages Nuxt 4** (AC: #1, #2) - [ ] Créer la structure `frontend/app/pages/` avec les pages de base : - `index.vue` (landing page - placeholder) - `projets/index.vue` (liste projets - placeholder) - `projets/[slug].vue` (détail projet - placeholder) - `competences.vue` (skills - placeholder) - `temoignages.vue` (testimonials - placeholder) - `parcours.vue` (journey - placeholder) - `contact.vue` (contact - placeholder) - `resume.vue` (mode express - placeholder) - [ ] Vérifier que le routing fonctionne avec les URLs localisées (Story 1.3) - [ ] **Task 2: Layout default.vue** (AC: #3) - [ ] Créer `frontend/app/layouts/default.vue` - [ ] Inclure le composant `AppHeader` (à créer) - [ ] Inclure le slot `` pour le contenu de page - [ ] Inclure le composant `AppFooter` (à créer) - [ ] Ajouter le wrapper pour les transitions de page - [ ] **Task 3: Composant AppHeader** (AC: #3) - [ ] Créer `frontend/app/components/layout/AppHeader.vue` - [ ] Navigation principale avec liens localisés (`localePath()`) - [ ] Placeholder pour la barre de progression (implémentée en Epic 3) - [ ] Intégrer le `LanguageSwitcher` (Story 1.3) - [ ] Logo/nom du site cliquable vers accueil - [ ] Version mobile : hamburger menu ou navigation adaptée - [ ] Sticky header avec fond semi-transparent sur scroll - [ ] **Task 4: Composant AppFooter** (AC: #3) - [ ] Créer `frontend/app/components/layout/AppFooter.vue` - [ ] Liens sociaux (GitHub, LinkedIn, etc.) - configurables via runtimeConfig - [ ] Copyright avec année dynamique - [ ] Liens secondaires (mentions légales si nécessaire) - [ ] Style cohérent avec sky-dark / sky-text - [ ] **Task 5: Layout minimal.vue** (AC: #4) - [ ] Créer `frontend/app/layouts/minimal.vue` - [ ] Header simplifié (logo + retour vers aventure) - [ ] Pas de barre de progression - [ ] Footer minimaliste - [ ] Utilisé pour `/resume` et potentiellement d'autres pages express - [ ] **Task 6: Transitions de page** (AC: #1, #6) - [ ] Configurer `pageTransition` dans `nuxt.config.ts` : ```typescript app: { pageTransition: { name: 'page', mode: 'out-in' } } ``` - [ ] Créer les styles CSS pour la transition `page` dans `assets/css/transitions.css` - [ ] Animation : fade-in/out + léger slide vertical (effet "changement de zone") - [ ] Durée : 300-400ms - [ ] Respecter `prefers-reduced-motion` : transition instantanée si activé - [ ] **Task 7: CSS des transitions** (AC: #1, #6) - [ ] Créer `frontend/app/assets/css/transitions.css` - [ ] Classes `.page-enter-active`, `.page-leave-active` - [ ] Classes `.page-enter-from`, `.page-leave-to` - [ ] Media query `@media (prefers-reduced-motion: reduce)` pour désactiver - [ ] Importer dans `nuxt.config.ts` via `css: ['~/assets/css/transitions.css']` - [ ] **Task 8: Scroll behavior personnalisé** (AC: #5) - [ ] Créer `frontend/app/router.options.ts` pour personnaliser le router - [ ] `scrollBehavior` : smooth scroll vers le haut pour nouvelle page - [ ] Sauvegarder et restaurer la position pour navigation back/forward - [ ] Gestion des ancres (`#section`) avec smooth scroll - [ ] **Task 9: Page d'erreur 404** (AC: #7) - [ ] Créer `frontend/app/error.vue` - [ ] Message d'erreur bilingue via `$t('error.404')` - [ ] Style immersif cohérent avec le thème (le narrateur pourrait commenter) - [ ] Bouton retour vers l'accueil (`localePath('/')`) - [ ] Gérer différents codes d'erreur (404, 500, etc.) - [ ] **Task 10: Meta tags SEO dynamiques** (AC: #8) - [ ] Créer composable `frontend/app/composables/useSeo.ts` - [ ] Méthode `setPageMeta({ title, description, image })` utilisant `useHead()` et `useSeoMeta()` - [ ] Inclure Open Graph tags (og:title, og:description, og:image, og:url) - [ ] Inclure Twitter Card tags - [ ] Utiliser dans chaque page avec des valeurs traduites - [ ] **Task 11: Favicon et assets statiques** (AC: #9) - [ ] Ajouter favicon dans `frontend/public/favicon.ico` - [ ] Ajouter favicon PNG 192x192 et 512x512 pour PWA - [ ] Configurer dans `nuxt.config.ts` via `app.head.link` - [ ] Optionnel : apple-touch-icon pour iOS - [ ] **Task 12: Validation finale** (AC: tous) - [ ] Navigation entre toutes les pages sans rechargement - [ ] Transitions visibles et fluides - [ ] `prefers-reduced-motion` respecté (tester dans DevTools) - [ ] Header sticky avec langue switcher fonctionnel - [ ] Layout minimal sur `/resume` - [ ] Page 404 accessible via URL invalide - [ ] Meta tags visibles dans le code source HTML - [ ] Favicon affiché dans l'onglet du navigateur ## Dev Notes ### Structure des layouts et pages ``` frontend/app/ ├── layouts/ │ ├── default.vue # Layout principal (header, footer, transitions) │ └── minimal.vue # Layout simplifié (mode express) ├── pages/ │ ├── index.vue # Landing page │ ├── projets/ │ │ ├── index.vue # Liste des projets │ │ └── [slug].vue # Détail projet │ ├── competences.vue # Page compétences │ ├── temoignages.vue # Page témoignages │ ├── parcours.vue # Page parcours │ ├── contact.vue # Page contact │ └── resume.vue # Mode express (layout minimal) ├── components/ │ └── layout/ │ ├── AppHeader.vue # Header avec navigation │ └── AppFooter.vue # Footer ├── error.vue # Page d'erreur globale └── assets/ └── css/ └── transitions.css # Styles des transitions ``` ### Configuration nuxt.config.ts pour les transitions ```typescript // frontend/nuxt.config.ts export default defineNuxtConfig({ // ... autres config app: { head: { link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'icon', type: 'image/png', sizes: '192x192', href: '/favicon-192.png' }, { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }, ], }, pageTransition: { name: 'page', mode: 'out-in', }, layoutTransition: { name: 'layout', mode: 'out-in', }, }, css: [ '~/assets/css/transitions.css', ], }) ``` ### CSS des transitions de page ```css /* frontend/app/assets/css/transitions.css */ /* Transition de page - effet "changement de zone" */ .page-enter-active, .page-leave-active { transition: opacity 0.3s ease, transform 0.3s ease; } .page-enter-from { opacity: 0; transform: translateY(10px); } .page-leave-to { opacity: 0; transform: translateY(-10px); } /* Transition de layout */ .layout-enter-active, .layout-leave-active { transition: opacity 0.2s ease; } .layout-enter-from, .layout-leave-to { opacity: 0; } /* Respect de prefers-reduced-motion */ @media (prefers-reduced-motion: reduce) { .page-enter-active, .page-leave-active, .layout-enter-active, .layout-leave-active { transition: none; } .page-enter-from, .page-leave-to, .layout-enter-from, .layout-leave-to { opacity: 1; transform: none; } } ``` ### Router options pour scroll behavior ```typescript // frontend/app/router.options.ts import type { RouterConfig } from '@nuxt/schema' export default { scrollBehavior(to, from, savedPosition) { // Si on revient en arrière, restaurer la position if (savedPosition) { return new Promise((resolve) => { setTimeout(() => { resolve(savedPosition) }, 350) // Attendre la fin de la transition }) } // Si on a une ancre, scroll vers l'ancre if (to.hash) { return { el: to.hash, behavior: 'smooth', top: 80, // Offset pour le header sticky } } // Sinon, scroll en haut return new Promise((resolve) => { setTimeout(() => { resolve({ top: 0, behavior: 'smooth' }) }, 350) }) }, } ``` ### Layout default.vue ```vue ``` ### Layout minimal.vue ```vue ``` ### Composable useSeo ```typescript // frontend/app/composables/useSeo.ts interface SeoOptions { title: string description?: string image?: string url?: string } export const useSeo = () => { const config = useRuntimeConfig() const route = useRoute() const { locale } = useI18n() const setPageMeta = (options: SeoOptions) => { const fullUrl = `${config.public.siteUrl}${route.fullPath}` const imageUrl = options.image || `${config.public.siteUrl}/og-image.jpg` useHead({ title: options.title, htmlAttrs: { lang: locale.value, }, }) useSeoMeta({ title: options.title, description: options.description, ogTitle: options.title, ogDescription: options.description, ogImage: imageUrl, ogUrl: options.url || fullUrl, ogLocale: locale.value === 'fr' ? 'fr_FR' : 'en_US', twitterCard: 'summary_large_image', twitterTitle: options.title, twitterDescription: options.description, twitterImage: imageUrl, }) } return { setPageMeta } } ``` ### Page d'erreur ```vue ``` ### Traductions à ajouter (i18n) ```json // Ajouter dans frontend/i18n/fr.json { "common": { "back_home": "Retour à l'accueil", "back_to_adventure": "Retour à l'aventure" }, "error": { "404": "Oups ! Cette page semble s'être perdue dans les méandres du code...", "generic": "Une erreur inattendue s'est produite. Le Bug enquête..." } } ``` ### Dépendances avec Stories précédentes **Cette story DÉPEND de :** - Story 1.1 : Nuxt 4 initialisé avec TailwindCSS et design tokens - Story 1.3 : Système i18n configuré, LanguageSwitcher créé **Cette story PRÉPARE pour :** - Story 1.5 : Landing page utilisera le layout default - Story 1.6 : Store Pinia intégrera la barre de progression dans AppHeader - Story 1.7 : Page résumé utilisera le layout minimal - Epic 2-4 : Toutes les pages utiliseront ces layouts ### Project Structure Notes **Fichiers à créer :** ``` frontend/app/ ├── layouts/ │ ├── default.vue # CRÉER │ └── minimal.vue # CRÉER ├── pages/ │ ├── index.vue # CRÉER (placeholder) │ ├── projets/ │ │ ├── index.vue # CRÉER (placeholder) │ │ └── [slug].vue # CRÉER (placeholder) │ ├── competences.vue # CRÉER (placeholder) │ ├── temoignages.vue # CRÉER (placeholder) │ ├── parcours.vue # CRÉER (placeholder) │ ├── contact.vue # CRÉER (placeholder) │ └── resume.vue # CRÉER (placeholder) ├── components/ │ └── layout/ │ ├── AppHeader.vue # CRÉER │ └── AppFooter.vue # CRÉER ├── composables/ │ └── useSeo.ts # CRÉER ├── error.vue # CRÉER ├── router.options.ts # CRÉER └── assets/ └── css/ └── transitions.css # CRÉER ``` **Fichiers à modifier :** ``` frontend/ ├── nuxt.config.ts # MODIFIER (transitions, css, head) ├── i18n/fr.json # MODIFIER (ajouter traductions) └── i18n/en.json # MODIFIER (ajouter traductions) ``` ### References - [Source: docs/planning-artifacts/architecture.md#Frontend-Architecture] - [Source: docs/planning-artifacts/architecture.md#Transitions-et-animations] - [Source: docs/planning-artifacts/ux-design-specification.md#Navigation] - [Source: docs/planning-artifacts/epics.md#Story-1.4] - [Source: docs/prd-gamification.md#FR2] - [Source: docs/prd-gamification.md#NFR6] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | Page transitions | fade + slide | FR2 | | Reduced motion | Required | NFR6, WCAG AA | | Sticky header | Yes | UX Design | | SEO meta tags | Required | NFR5 | | Layout switching | default / minimal | Architecture | ## Dev Agent Record ### Agent Model Used {{agent_model_name_version}} ### Debug Log References ### Completion Notes List ### Change Log | Date | Change | Author | |------|--------|--------| | 2026-02-03 | Story créée avec contexte complet | SM Agent | ### File List