# Story 2.3: Page Projet - Détail

Status: review

## Story

As a visiteur,
I want voir les détails d'un projet spécifique,
so that je comprends le travail réalisé et les technologies utilisées.

## Acceptance Criteria

1. **Given** le visiteur accède à `/projets/{slug}` (FR) ou `/en/projects/{slug}` (EN) **When** la page se charge **Then** le titre, la description complète et l'image principale du projet s'affichent
2. **And** la date de réalisation est visible
3. **And** la liste des compétences utilisées s'affiche avec leurs niveaux (avant/après le projet)
4. **And** les liens externes sont présents : URL du projet live (si existe), repository GitHub (si existe)
5. **And** une navigation "Projet précédent / Projet suivant" permet de parcourir les projets
6. **And** un bouton retour vers la galerie est visible
7. **And** les meta tags SEO sont dynamiques (titre, description, image Open Graph)
8. **And** si le slug n'existe pas, une page 404 appropriée s'affiche
9. **And** le design est responsive (adaptation mobile/desktop)

## Tasks / Subtasks

- [x] **Task 1: Créer l'endpoint API pour le détail du projet** (AC: #1, #2, #3, #4, #8)
  - [x] Ajouter la méthode `show($slug)` dans `ProjectController`
  - [x] Charger le projet avec ses compétences (eager loading)
  - [x] Retourner 404 si le slug n'existe pas
  - [x] Inclure les données de traduction selon `Accept-Language`

- [x] **Task 2: Créer l'endpoint API pour la navigation prev/next** (AC: #5)
  - [x] Ajouter une méthode `navigation($slug)` ou inclure dans `show()`
  - [x] Retourner le projet précédent et suivant (basé sur l'ordre de tri)
  - [x] Si premier projet : prev = null, si dernier : next = null

- [x] **Task 3: Créer le composable useFetchProject** (AC: #1)
  - [x] Créer `frontend/app/composables/useFetchProject.ts`
  - [x] Accepter le slug en paramètre
  - [x] Gérer les états loading, error, data
  - [x] Gérer l'erreur 404

- [x] **Task 4: Créer la page [slug].vue** (AC: #1, #2, #3, #4, #6, #9)
  - [x] Créer `frontend/app/pages/projets/[slug].vue`
  - [x] Afficher l'image principale en grand format
  - [x] Afficher le titre et la description complète
  - [x] Afficher la date de réalisation formatée
  - [x] Afficher la liste des compétences avec progression (avant → après)
  - [x] Afficher les liens externes (site live, GitHub) si présents
  - [x] Ajouter un bouton "Retour à la galerie"

- [x] **Task 5: Implémenter la navigation prev/next** (AC: #5)
  - [x] Ajouter les boutons "Projet précédent" et "Projet suivant"
  - [x] Utiliser NuxtLink pour la navigation
  - [x] Afficher le titre du projet dans le bouton
  - [x] Désactiver/masquer si pas de prev ou next

- [x] **Task 6: Meta tags SEO dynamiques** (AC: #7)
  - [x] Utiliser `useHead()` avec le titre du projet
  - [x] Utiliser `useSeoMeta()` pour description, og:title, og:description, og:image
  - [x] L'image OG doit être l'image du projet

- [x] **Task 7: Gestion de l'erreur 404** (AC: #8)
  - [x] Détecter si le projet n'existe pas
  - [x] Afficher un message approprié avec le narrateur
  - [x] Proposer de retourner à la galerie

- [x] **Task 8: Design responsive** (AC: #9)
  - [x] Mobile : layout vertical, image pleine largeur
  - [x] Desktop : layout 2 colonnes (image + contenu) ou grande image + contenu dessous
  - [x] Liste des compétences responsive (flex wrap)

- [x] **Task 9: Tests et validation**
  - [x] Tester avec différents slugs de projets
  - [x] Tester la navigation prev/next
  - [x] Tester le 404 avec un slug inexistant
  - [x] Valider les meta tags SEO
  - [x] Tester le responsive

## Dev Notes

### Endpoint API Laravel

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

public function show(Request $request, string $slug)
{
    $lang = $request->header('Accept-Language', 'fr');

    $project = Project::with('skills')
        ->where('slug', $slug)
        ->first();

    if (!$project) {
        return response()->json([
            'error' => [
                'code' => 'PROJECT_NOT_FOUND',
                'message' => 'Project not found',
            ]
        ], 404);
    }

    // Navigation prev/next
    $allProjects = Project::orderByDesc('is_featured')
        ->orderByDesc('date_completed')
        ->get(['id', 'slug', 'title_key']);

    $currentIndex = $allProjects->search(fn ($p) => $p->slug === $slug);

    $prev = $currentIndex > 0 ? $allProjects[$currentIndex - 1] : null;
    $next = $currentIndex < $allProjects->count() - 1 ? $allProjects[$currentIndex + 1] : null;

    return (new ProjectResource($project))->additional([
        'meta' => [
            'lang' => $lang,
        ],
        'navigation' => [
            'prev' => $prev ? [
                'slug' => $prev->slug,
                'title' => Translation::getTranslation($prev->title_key, $lang),
            ] : null,
            'next' => $next ? [
                'slug' => $next->slug,
                'title' => Translation::getTranslation($next->title_key, $lang),
            ] : null,
        ],
    ]);
}
```

```php
// api/routes/api.php
Route::get('/projects/{slug}', [ProjectController::class, 'show']);
```

### Composable useFetchProject

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

interface ProjectNavigation {
  prev: { slug: string; title: string } | null
  next: { slug: string; title: string } | null
}

interface ProjectResponse {
  data: Project
  meta: { lang: string }
  navigation: ProjectNavigation
}

export function useFetchProject(slug: string | Ref<string>) {
  const config = useRuntimeConfig()
  const { locale } = useI18n()
  const slugValue = toValue(slug)

  return useFetch<ProjectResponse>(`/projects/${slugValue}`, {
    baseURL: config.public.apiUrl,
    headers: {
      'X-API-Key': config.public.apiKey,
      'Accept-Language': locale.value,
    },
  })
}
```

### Page [slug].vue

```vue
<!-- frontend/app/pages/projets/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const { t, d } = useI18n()
const localePath = useLocalePath()

const slug = computed(() => route.params.slug as string)
const { data, pending, error } = useFetchProject(slug)

const project = computed(() => data.value?.data)
const navigation = computed(() => data.value?.navigation)

// SEO dynamique
useHead({
  title: () => project.value?.title ? `${project.value.title} | Skycel` : t('projects.loading'),
})

useSeoMeta({
  title: () => project.value?.title,
  description: () => project.value?.shortDescription,
  ogTitle: () => project.value?.title,
  ogDescription: () => project.value?.shortDescription,
  ogImage: () => project.value?.image,
})
</script>

<template>
  <div class="container mx-auto px-4 py-8">
    <!-- Loading -->
    <div v-if="pending" class="animate-pulse">
      <div class="bg-sky-dark-50 rounded-lg h-64 mb-6"></div>
      <div class="bg-sky-dark-50 h-10 rounded w-1/2 mb-4"></div>
      <div class="bg-sky-dark-50 h-4 rounded w-full mb-2"></div>
      <div class="bg-sky-dark-50 h-4 rounded w-3/4"></div>
    </div>

    <!-- Error 404 -->
    <div v-else-if="error" class="text-center py-16">
      <h1 class="text-2xl font-ui font-bold text-sky-text mb-4">
        {{ t('projects.notFound') }}
      </h1>
      <p class="text-sky-text-muted mb-6">
        {{ t('projects.notFoundDescription') }}
      </p>
      <NuxtLink
        :to="localePath('/projets')"
        class="bg-sky-accent text-white px-6 py-3 rounded-lg hover:bg-sky-accent-hover inline-block"
      >
        {{ t('projects.backToGallery') }}
      </NuxtLink>
    </div>

    <!-- Project content -->
    <article v-else-if="project">
      <!-- Retour galerie -->
      <NuxtLink
        :to="localePath('/projets')"
        class="inline-flex items-center text-sky-text-muted hover:text-sky-accent mb-6 transition-colors"
      >
        <span class="mr-2">←</span>
        {{ t('projects.backToGallery') }}
      </NuxtLink>

      <!-- Image principale -->
      <div class="mb-8">
        <NuxtImg
          :src="project.image"
          :alt="project.title"
          format="webp"
          class="w-full h-auto max-h-96 object-cover rounded-lg"
        />
      </div>

      <!-- Titre et date -->
      <header class="mb-6">
        <h1 class="text-3xl md:text-4xl font-ui font-bold text-sky-text mb-2">
          {{ project.title }}
        </h1>
        <p class="text-sky-text-muted">
          {{ t('projects.completedOn') }} {{ d(new Date(project.dateCompleted), 'long') }}
        </p>
      </header>

      <!-- Description -->
      <div class="prose prose-invert max-w-none mb-8">
        <p class="text-sky-text text-lg leading-relaxed">
          {{ project.description }}
        </p>
      </div>

      <!-- Liens externes -->
      <div v-if="project.url || project.githubUrl" class="flex flex-wrap gap-4 mb-8">
        <a
          v-if="project.url"
          :href="project.url"
          target="_blank"
          rel="noopener noreferrer"
          class="inline-flex items-center bg-sky-accent text-white px-6 py-3 rounded-lg hover:bg-sky-accent-hover transition-colors"
        >
          🌐 {{ t('projects.visitSite') }}
        </a>
        <a
          v-if="project.githubUrl"
          :href="project.githubUrl"
          target="_blank"
          rel="noopener noreferrer"
          class="inline-flex items-center bg-sky-dark-50 text-sky-text px-6 py-3 rounded-lg hover:bg-sky-dark-100 transition-colors border border-sky-dark-100"
        >
          💻 {{ t('projects.viewCode') }}
        </a>
      </div>

      <!-- Compétences utilisées -->
      <section v-if="project.skills?.length" class="mb-12">
        <h2 class="text-xl font-ui font-semibold text-sky-text mb-4">
          {{ t('projects.skillsUsed') }}
        </h2>
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          <div
            v-for="skill in project.skills"
            :key="skill.id"
            class="bg-sky-dark-50 rounded-lg p-4"
          >
            <div class="font-ui font-medium text-sky-text">{{ skill.name }}</div>
            <div class="text-sm text-sky-text-muted mt-1">
              {{ t('projects.skillLevel') }}:
              <span class="text-sky-accent">{{ skill.levelBefore }}</span>
              →
              <span class="text-sky-accent font-semibold">{{ skill.levelAfter }}</span>
            </div>
          </div>
        </div>
      </section>

      <!-- Navigation prev/next -->
      <nav class="flex justify-between items-center border-t border-sky-dark-100 pt-8 mt-8">
        <NuxtLink
          v-if="navigation?.prev"
          :to="localePath(`/projets/${navigation.prev.slug}`)"
          class="flex flex-col text-left hover:text-sky-accent transition-colors"
        >
          <span class="text-sm text-sky-text-muted">{{ t('projects.previous') }}</span>
          <span class="text-sky-text">← {{ navigation.prev.title }}</span>
        </NuxtLink>
        <div v-else></div>

        <NuxtLink
          v-if="navigation?.next"
          :to="localePath(`/projets/${navigation.next.slug}`)"
          class="flex flex-col text-right hover:text-sky-accent transition-colors"
        >
          <span class="text-sm text-sky-text-muted">{{ t('projects.next') }}</span>
          <span class="text-sky-text">{{ navigation.next.title }} →</span>
        </NuxtLink>
        <div v-else></div>
      </nav>
    </article>
  </div>
</template>
```

### Clés i18n nécessaires

**fr.json :**
```json
{
  "projects": {
    "loading": "Chargement...",
    "notFound": "Projet introuvable",
    "notFoundDescription": "Ce projet n'existe pas ou a été supprimé.",
    "backToGallery": "Retour à la galerie",
    "completedOn": "Réalisé le",
    "visitSite": "Voir le site",
    "viewCode": "Voir le code",
    "skillsUsed": "Compétences utilisées",
    "skillLevel": "Niveau",
    "previous": "Projet précédent",
    "next": "Projet suivant"
  }
}
```

**en.json :**
```json
{
  "projects": {
    "loading": "Loading...",
    "notFound": "Project not found",
    "notFoundDescription": "This project doesn't exist or has been removed.",
    "backToGallery": "Back to gallery",
    "completedOn": "Completed on",
    "visitSite": "Visit site",
    "viewCode": "View code",
    "skillsUsed": "Skills used",
    "skillLevel": "Level",
    "previous": "Previous project",
    "next": "Next project"
  }
}
```

### Configuration i18n pour les dates

Ajouter dans `nuxt.config.ts` la configuration des formats de date :

```typescript
i18n: {
  datetimeFormats: {
    fr: {
      long: { year: 'numeric', month: 'long', day: 'numeric' }
    },
    en: {
      long: { year: 'numeric', month: 'long', day: 'numeric' }
    }
  }
}
```

### Dépendances

**Cette story nécessite :**
- Story 2.1 : Composant ProjectCard
- Story 2.2 : Endpoint API `/api/projects` et page galerie

**Cette story prépare pour :**
- Story 2.5 : Compétences cliquables (liens vers projets)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── pages/
│   └── projets/
│       └── [slug].vue           # CRÉER
└── composables/
    └── useFetchProject.ts       # CRÉER
```

**Fichiers à modifier :**
```
api/app/Http/Controllers/Api/ProjectController.php  # AJOUTER show()
api/routes/api.php                                   # AJOUTER route
frontend/i18n/fr.json                                # AJOUTER clés
frontend/i18n/en.json                                # AJOUTER clés
frontend/nuxt.config.ts                              # AJOUTER datetimeFormats
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-2.3]
- [Source: docs/planning-artifacts/architecture.md#API-&-Communication-Patterns]
- [Source: docs/planning-artifacts/ux-design-specification.md#Responsive-Strategy]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Route dynamique | /projets/[slug] | Nuxt routing |
| API endpoint | GET /api/projects/{slug} | Architecture |
| Navigation | prev/next avec titres | Epics |
| SEO | Meta dynamiques + OG image | NFR5 |
| 404 | Message approprié | Epics |

## Dev Agent Record

### Agent Model Used

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

### Debug Log References

- Aucun problème. Méthode show() existait déjà mais sans navigation prev/next.

### Completion Notes List

- ProjectController show() amélioré avec navigation prev/next et erreur 404 structurée
- Composable useFetchProject créé avec typage ProjectNavigation
- Page [slug].vue complète avec :
  - Image principale pleine largeur
  - Titre, date formatée selon locale, badge featured
  - Description complète
  - Liens externes (site et GitHub)
  - Grille responsive des compétences utilisées avec niveaux avant/après
  - Navigation prev/next avec titres traduits
  - État loading (skeleton), état 404 avec narrateur spider
- SEO dynamique via useSeo avec og:image du projet
- Formatage date via Intl.DateTimeFormat selon locale
- Traductions FR/EN ajoutées (10 nouvelles clés projects.*)
- Store progression visitSection('projets') sur mount

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

### File List

- `api/app/Http/Controllers/Api/ProjectController.php` — MODIFIÉ (show avec navigation)
- `frontend/app/composables/useFetchProject.ts` — CRÉÉ
- `frontend/app/pages/projets/[slug].vue` — RÉÉCRIT
- `frontend/i18n/fr.json` — MODIFIÉ (ajout projects.*)
- `frontend/i18n/en.json` — MODIFIÉ (ajout projects.*)

