Files
Portfolio-Game/docs/implementation-artifacts/2-2-page-projets-galerie.md
skycel ec1ae92799 🎉 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>
2026-02-05 02:08:56 +01:00

11 KiB

Story 2.2: Page Projets - Galerie

Status: ready-for-dev

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

  • Task 1: Créer l'endpoint API Laravel (AC: #4)

    • Créer app/Http/Controllers/Api/ProjectController.php
    • Créer la méthode index() pour lister tous les projets
    • Implémenter le tri : featured en premier, puis par date_completed DESC
    • Joindre les traductions selon le header Accept-Language
    • Créer app/Http/Resources/ProjectResource.php pour formater la réponse
    • Ajouter la route GET /api/projects dans routes/api.php
  • Task 2: Créer le composable useFetchProjects (AC: #4)

    • Créer frontend/app/composables/useFetchProjects.ts
    • Utiliser useFetch() pour appeler l'API avec le header Accept-Language
    • Gérer les états loading, error, data
    • Typer la réponse avec l'interface Project[]
  • Task 3: Créer la page projets.vue (AC: #1, #6)

    • Créer frontend/app/pages/projets.vue
    • Utiliser le composable useFetchProjects() pour charger les données
    • Afficher une grille de ProjectCard avec les données
    • Implémenter le layout responsive : 1 colonne mobile, 2 tablette, 3-4 desktop
  • Task 4: Implémenter l'animation d'entrée (AC: #3)

    • Animer l'apparition progressive des cards (stagger animation)
    • Utiliser CSS animations ou GSAP pour un effet fade-in + slide-up
    • Respecter prefers-reduced-motion : pas d'animation si activé
    • Délai de 50-100ms entre chaque card
  • Task 5: Tri des projets (AC: #2)

    • S'assurer que l'API retourne les projets dans le bon ordre
    • Vérifier côté frontend que l'ordre est respecté
    • Les projets is_featured: true apparaissent en premier
    • Puis tri par date_completed DESC
  • Task 6: Meta tags SEO (AC: #5)

    • Utiliser useHead() pour définir le titre dynamique
    • Utiliser useSeoMeta() pour les meta description, og:title, og:description
    • Ajouter les clés i18n pour titre et description de la page
    • Exemple titre : "Projets | Skycel" / "Projects | Skycel"
  • Task 7: État loading et erreur

    • Afficher un skeleton/loading state pendant le chargement
    • Afficher un message d'erreur narratif si l'API échoue
    • Bouton "Réessayer" en cas d'erreur
  • Task 8: Tests et validation

    • Tester la page en FR et EN
    • Vérifier le tri des projets
    • Tester l'animation d'entrée
    • Valider le responsive sur mobile/tablette/desktop
    • Vérifier les meta tags avec l'inspecteur

Dev Notes

Endpoint API Laravel

<?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
// 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,
                ]);
            }),
        ];
    }
}
// api/routes/api.php
Route::get('/projects', [ProjectController::class, 'index']);

Composable useFetchProjects

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

<!-- 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 :

{
  "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 :

{
  "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

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