# Story 2.2: Page Projets - Galerie

Status: review

## Story

As a visiteur,
I want voir la liste des projets réalisés par le développeur,
so that je peux évaluer son expérience et choisir lesquels explorer en détail.

## Acceptance Criteria

1. **Given** le visiteur accède à `/projets` (FR) ou `/en/projects` (EN) **When** la page se charge **Then** une grille responsive de `ProjectCard` s'affiche
2. **And** les projets sont triés par date avec les "featured" en tête
3. **And** une animation d'entrée progressive des cards est présente (respectant `prefers-reduced-motion`)
4. **And** les données sont chargées depuis l'API `/api/projects` avec le contenu traduit
5. **And** les meta tags SEO sont dynamiques pour cette page
6. **And** le layout s'adapte : grille sur desktop, cards empilées sur mobile

## Tasks / Subtasks

- [x] **Task 1: Créer l'endpoint API Laravel** (AC: #4)
  - [x] Créer `app/Http/Controllers/Api/ProjectController.php`
  - [x] Créer la méthode `index()` pour lister tous les projets
  - [x] Implémenter le tri : featured en premier, puis par date_completed DESC
  - [x] Joindre les traductions selon le header `Accept-Language`
  - [x] Créer `app/Http/Resources/ProjectResource.php` pour formater la réponse
  - [x] Ajouter la route `GET /api/projects` dans `routes/api.php`

- [x] **Task 2: Créer le composable useFetchProjects** (AC: #4)
  - [x] Créer `frontend/app/composables/useFetchProjects.ts`
  - [x] Utiliser `useFetch()` pour appeler l'API avec le header `Accept-Language`
  - [x] Gérer les états loading, error, data
  - [x] Typer la réponse avec l'interface Project[]

- [x] **Task 3: Créer la page projets.vue** (AC: #1, #6)
  - [x] Créer `frontend/app/pages/projets.vue`
  - [x] Utiliser le composable `useFetchProjects()` pour charger les données
  - [x] Afficher une grille de `ProjectCard` avec les données
  - [x] Implémenter le layout responsive : 1 colonne mobile, 2 tablette, 3-4 desktop

- [x] **Task 4: Implémenter l'animation d'entrée** (AC: #3)
  - [x] Animer l'apparition progressive des cards (stagger animation)
  - [x] Utiliser CSS animations ou GSAP pour un effet fade-in + slide-up
  - [x] Respecter `prefers-reduced-motion` : pas d'animation si activé
  - [x] Délai de 50-100ms entre chaque card

- [x] **Task 5: Tri des projets** (AC: #2)
  - [x] S'assurer que l'API retourne les projets dans le bon ordre
  - [x] Vérifier côté frontend que l'ordre est respecté
  - [x] Les projets `is_featured: true` apparaissent en premier
  - [x] Puis tri par `date_completed` DESC

- [x] **Task 6: Meta tags SEO** (AC: #5)
  - [x] Utiliser `useHead()` pour définir le titre dynamique
  - [x] Utiliser `useSeoMeta()` pour les meta description, og:title, og:description
  - [x] Ajouter les clés i18n pour titre et description de la page
  - [x] Exemple titre : "Projets | Skycel" / "Projects | Skycel"

- [x] **Task 7: État loading et erreur**
  - [x] Afficher un skeleton/loading state pendant le chargement
  - [x] Afficher un message d'erreur narratif si l'API échoue
  - [x] Bouton "Réessayer" en cas d'erreur

- [x] **Task 8: Tests et validation**
  - [x] Tester la page en FR et EN
  - [x] Vérifier le tri des projets
  - [x] Tester l'animation d'entrée
  - [x] Valider le responsive sur mobile/tablette/desktop
  - [x] Vérifier les meta tags avec l'inspecteur

## Dev Notes

### Endpoint API Laravel

```php
<?php
// api/app/Http/Controllers/Api/ProjectController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\ProjectResource;
use App\Models\Project;
use Illuminate\Http\Request;

class ProjectController extends Controller
{
    public function index(Request $request)
    {
        $lang = $request->header('Accept-Language', 'fr');

        $projects = Project::query()
            ->with(['skills'])
            ->orderByDesc('is_featured')
            ->orderByDesc('date_completed')
            ->get();

        return ProjectResource::collection($projects)
            ->additional(['meta' => ['lang' => $lang]]);
    }
}
```

```php
<?php
// api/app/Http/Resources/ProjectResource.php

namespace App\Http\Resources;

use App\Models\Translation;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProjectResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        $lang = $request->header('Accept-Language', 'fr');

        return [
            'id' => $this->id,
            'slug' => $this->slug,
            'title' => Translation::getTranslation($this->title_key, $lang),
            'description' => Translation::getTranslation($this->description_key, $lang),
            'shortDescription' => Translation::getTranslation($this->short_description_key, $lang),
            'image' => $this->image,
            'url' => $this->url,
            'githubUrl' => $this->github_url,
            'dateCompleted' => $this->date_completed?->format('Y-m-d'),
            'isFeatured' => $this->is_featured,
            'displayOrder' => $this->display_order,
            'skills' => $this->whenLoaded('skills', function () use ($lang) {
                return $this->skills->map(fn ($skill) => [
                    'id' => $skill->id,
                    'slug' => $skill->slug,
                    'name' => Translation::getTranslation($skill->name_key, $lang),
                    'levelBefore' => $skill->pivot->level_before,
                    'levelAfter' => $skill->pivot->level_after,
                ]);
            }),
        ];
    }
}
```

```php
// api/routes/api.php
Route::get('/projects', [ProjectController::class, 'index']);
```

### Composable useFetchProjects

```typescript
// frontend/app/composables/useFetchProjects.ts
import type { Project } from '~/types/project'

export function useFetchProjects() {
  const config = useRuntimeConfig()
  const { locale } = useI18n()

  return useFetch<{ data: Project[], meta: { lang: string } }>('/projects', {
    baseURL: config.public.apiUrl,
    headers: {
      'X-API-Key': config.public.apiKey,
      'Accept-Language': locale.value,
    },
    transform: (response) => response.data,
  })
}
```

### Page Projets

```vue
<!-- frontend/app/pages/projets.vue -->
<script setup lang="ts">
const { t } = useI18n()
const { data: projects, pending, error, refresh } = useFetchProjects()

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

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

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

    <!-- Loading state -->
    <div v-if="pending" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <div v-for="i in 6" :key="i" class="animate-pulse">
        <div class="bg-sky-dark-50 rounded-lg h-48"></div>
        <div class="p-4">
          <div class="bg-sky-dark-50 h-6 rounded w-3/4 mb-2"></div>
          <div class="bg-sky-dark-50 h-4 rounded w-full"></div>
        </div>
      </div>
    </div>

    <!-- Error state -->
    <div v-else-if="error" class="text-center py-12">
      <p class="text-sky-text-muted mb-4">{{ t('projects.loadError') }}</p>
      <button
        @click="refresh()"
        class="bg-sky-accent text-white px-6 py-2 rounded-lg hover:bg-sky-accent-hover"
      >
        {{ t('common.retry') }}
      </button>
    </div>

    <!-- Projects grid -->
    <div
      v-else
      class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
    >
      <ProjectCard
        v-for="(project, index) in projects"
        :key="project.id"
        :project="project"
        class="project-card-animated"
        :style="{ '--animation-delay': `${index * 100}ms` }"
      />
    </div>
  </div>
</template>

<style scoped>
.project-card-animated {
  animation: fadeInUp 0.5s ease-out forwards;
  animation-delay: var(--animation-delay, 0ms);
  opacity: 0;
}

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

/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .project-card-animated {
    animation: none;
    opacity: 1;
  }
}
</style>
```

### Clés i18n nécessaires

**fr.json :**
```json
{
  "projects": {
    "title": "Mes Projets",
    "pageTitle": "Projets | Skycel",
    "pageDescription": "Découvrez les projets réalisés par Célian, développeur web full-stack.",
    "discover": "Découvrir",
    "loadError": "Impossible de charger les projets..."
  },
  "common": {
    "retry": "Réessayer"
  }
}
```

**en.json :**
```json
{
  "projects": {
    "title": "My Projects",
    "pageTitle": "Projects | Skycel",
    "pageDescription": "Discover projects created by Célian, full-stack web developer.",
    "discover": "Discover",
    "loadError": "Unable to load projects..."
  },
  "common": {
    "retry": "Retry"
  }
}
```

### Layout responsive

| Breakpoint | Colonnes | Gap |
|------------|----------|-----|
| Mobile (< 768px) | 1 | 24px |
| Tablette (768px - 1023px) | 2 | 24px |
| Desktop (1024px - 1279px) | 3 | 24px |
| Large (≥ 1280px) | 4 | 24px |

### Dépendances

**Cette story nécessite :**
- Story 1.1 : Nuxt 4 + Laravel 12 initialisés
- Story 1.2 : Table projects, Model Project avec relations
- Story 1.3 : Système i18n configuré
- Story 1.4 : Layouts et routing
- Story 2.1 : Composant ProjectCard

**Cette story prépare pour :**
- Story 2.3 : Page Projet - Détail (navigation depuis la galerie)

### Project Structure Notes

**Fichiers à créer :**
```
api/app/Http/
├── Controllers/Api/
│   └── ProjectController.php    # CRÉER
└── Resources/
    └── ProjectResource.php      # CRÉER

frontend/app/
├── pages/
│   └── projets.vue              # CRÉER
└── composables/
    └── useFetchProjects.ts      # CRÉER
```

**Fichiers à modifier :**
```
api/routes/api.php               # AJOUTER route /projects
frontend/i18n/fr.json            # AJOUTER clés projects.*
frontend/i18n/en.json            # AJOUTER clés projects.*
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-2.2]
- [Source: docs/planning-artifacts/architecture.md#API-&-Communication-Patterns]
- [Source: docs/planning-artifacts/architecture.md#Frontend-Architecture]
- [Source: docs/planning-artifacts/ux-design-specification.md#Screen-Architecture-Summary]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| API endpoint | GET /api/projects | Architecture |
| Response format | { data: [], meta: {} } | Architecture |
| Header langue | Accept-Language | Architecture |
| Animation | Stagger fade-in | Epics |
| SEO | Meta tags dynamiques | NFR5 |

### Previous Story Intelligence

**Patterns établis à suivre :**
- Controllers API dans `app/Http/Controllers/Api/`
- Resources dans `app/Http/Resources/`
- Composables dans `app/composables/`
- Pages dans `app/pages/`
- Utiliser `useFetch()` avec `baseURL` et headers

## Dev Agent Record

### Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

### Debug Log References

- Aucun problème. API ProjectController existait déjà (Story 1.3), scope ordered() mis à jour.

### Completion Notes List

- Scope `ordered()` mis à jour : featured DESC, date_completed DESC, display_order
- Composable `useFetchProjects` créé avec typage et transform
- Page projets complète avec grille responsive (1/2/3/4 colonnes selon breakpoint)
- Animation stagger fadeInUp avec délai 80ms entre cards
- prefers-reduced-motion respecté
- États loading (skeleton), error (avec retry), empty
- SEO meta tags dynamiques via useSeo()
- Intégration store progression (visitSection sur mount)
- Traductions FR/EN ajoutées (title, page_title, page_description, load_error, retry)

### Change Log
| Date | Change | Author |
|------|--------|--------|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |
| 2026-02-06 | Tasks 1-8 implémentées et validées | Dev Agent (Claude Opus 4.5) |

### File List

- `api/app/Models/Project.php` — MODIFIÉ (scope ordered amélioré)
- `frontend/app/composables/useFetchProjects.ts` — CRÉÉ
- `frontend/app/pages/projets/index.vue` — RÉÉCRIT (page complète)
- `frontend/i18n/fr.json` — MODIFIÉ (ajout projects.*, common.retry)
- `frontend/i18n/en.json` — MODIFIÉ (ajout projects.*, common.retry)

