Files
Portfolio-Game/docs/implementation-artifacts/2-1-composant-projectcard.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

8.3 KiB

Story 2.1: Composant ProjectCard

Status: ready-for-dev

Story

As a développeur, I want un composant réutilisable de card de projet, so that je peux afficher les projets de manière cohérente sur la galerie et ailleurs dans le site.

Acceptance Criteria

  1. Given le composant ProjectCard est implémenté When il reçoit les données d'un projet en props Then il affiche l'image du projet (WebP, lazy loading)
  2. And il affiche le titre traduit selon la langue courante
  3. And il affiche la description courte traduite
  4. And un hover effect révèle un CTA "Découvrir" avec animation subtile
  5. And le composant est cliquable et navigue vers /projets/{slug} (ou /en/projects/{slug})
  6. And le composant respecte prefers-reduced-motion pour les animations
  7. And le composant est responsive (adaptation mobile/desktop)
  8. And le composant est accessible (focus visible, role approprié)

Tasks / Subtasks

  • Task 1: Créer le composant ProjectCard.vue (AC: #1, #2, #3)

    • Créer le fichier frontend/app/components/feature/ProjectCard.vue
    • Définir les props TypeScript : project (object avec slug, image, title, shortDescription)
    • Utiliser <NuxtImg> pour l'image avec format WebP et lazy loading
    • Intégrer useI18n() pour le titre et la description traduits
    • Afficher titre (project.title) et description courte (project.shortDescription)
  • Task 2: Implémenter le hover effect et CTA (AC: #4)

    • Créer un overlay qui apparaît au hover avec transition CSS
    • Ajouter un CTA "Découvrir" (traduit via i18n) centré dans l'overlay
    • Animation subtile : fade-in + léger scale (0.98 → 1)
    • Utiliser les classes Tailwind pour les transitions
  • Task 3: Implémenter la navigation (AC: #5)

    • Rendre la card entièrement cliquable avec <NuxtLink>
    • Utiliser localePath() pour générer l'URL correcte selon la langue
    • URL pattern : /projets/{slug} (FR) ou /en/projects/{slug} (EN)
  • Task 4: Gérer prefers-reduced-motion (AC: #6)

    • Créer une media query CSS pour détecter prefers-reduced-motion: reduce
    • Désactiver les transitions et animations si motion réduite
    • Le hover effect reste visible mais sans animation
  • Task 5: Rendre le composant responsive (AC: #7)

    • Mobile : card pleine largeur, hauteur fixe ou aspect-ratio
    • Desktop : card avec largeur flexible pour grille (min 280px, max 400px)
    • Image qui remplit la card avec object-cover
    • Texte tronqué si trop long (ellipsis)
  • Task 6: Accessibilité (AC: #8)

    • Focus visible sur la card (outline accent)
    • role="article" sur la card container
    • alt descriptif sur l'image (utiliser le titre du projet)
    • Navigation au clavier fonctionnelle (Tab, Enter)
  • Task 7: Tests et validation

    • Tester le composant avec des données de projet fictives
    • Vérifier l'affichage en FR et EN
    • Vérifier le hover effect et la navigation
    • Tester sur mobile et desktop
    • Valider l'accessibilité avec axe DevTools

Dev Notes

Structure du composant

<!-- frontend/app/components/feature/ProjectCard.vue -->
<script setup lang="ts">
interface Project {
  slug: string
  image: string
  title: string
  shortDescription: string
}

const props = defineProps<{
  project: Project
}>()

const { t } = useI18n()
const localePath = useLocalePath()
</script>

<template>
  <NuxtLink
    :to="localePath(`/projets/${project.slug}`)"
    class="project-card group"
    role="article"
  >
    <div class="relative overflow-hidden rounded-lg">
      <!-- Image avec lazy loading -->
      <NuxtImg
        :src="project.image"
        :alt="project.title"
        format="webp"
        loading="lazy"
        class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105"
      />

      <!-- Overlay au hover -->
      <div class="absolute inset-0 bg-sky-dark/70 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
        <span class="text-sky-accent font-ui text-lg">
          {{ t('projects.discover') }}
        </span>
      </div>
    </div>

    <!-- Contenu texte -->
    <div class="p-4">
      <h3 class="font-ui text-lg text-sky-text font-semibold truncate">
        {{ project.title }}
      </h3>
      <p class="font-ui text-sm text-sky-text-muted line-clamp-2 mt-1">
        {{ project.shortDescription }}
      </p>
    </div>
  </NuxtLink>
</template>

<style scoped>
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .project-card * {
    transition: none !important;
    animation: none !important;
  }

  .project-card:hover img {
    transform: none;
  }
}

/* Focus visible */
.project-card:focus-visible {
  outline: 2px solid theme('colors.sky-accent.DEFAULT');
  outline-offset: 2px;
  border-radius: 0.5rem;
}
</style>

Interface TypeScript pour Project

// frontend/app/types/project.ts
export interface Project {
  id: number
  slug: string
  image: string
  title: string           // Déjà traduit par l'API
  description: string     // Déjà traduit par l'API
  shortDescription: string // Déjà traduit par l'API
  url?: string
  githubUrl?: string
  dateCompleted: string
  isFeatured: boolean
  displayOrder: number
  skills?: ProjectSkill[]
}

export interface ProjectSkill {
  id: number
  slug: string
  name: string
  levelBefore: number
  levelAfter: number
}

Clés i18n nécessaires

Ajouter dans frontend/i18n/fr.json et frontend/i18n/en.json :

{
  "projects": {
    "discover": "Découvrir"
  }
}
{
  "projects": {
    "discover": "Discover"
  }
}

Design Tokens utilisés

Token Valeur Usage
sky-dark Fond sombre Overlay au hover
sky-accent #fa784f CTA "Découvrir"
sky-text Blanc cassé Titre projet
sky-text-muted Variante atténuée Description courte
font-ui Inter Tout le texte du composant

Comportement responsive

Breakpoint Comportement
Mobile (< 768px) Card pleine largeur, hauteur image 180px
Tablette (768px+) Cards en grille 2 colonnes
Desktop (1024px+) Cards en grille 3-4 colonnes

Dépendances

Ce composant nécessite :

  • Story 1.1 : Nuxt 4 initialisé avec @nuxt/image, @nuxtjs/i18n, TailwindCSS
  • Story 1.2 : Model Project avec structure de données
  • Story 1.3 : Système i18n configuré

Ce composant sera utilisé par :

  • Story 2.2 : Page Projets - Galerie
  • Story 1.7 : Page Résumé Express (projets highlights)
  • Story 2.5 : Compétences cliquables (liste des projets liés)

Project Structure Notes

Fichiers à créer :

frontend/app/
├── components/
│   └── feature/
│       └── ProjectCard.vue    # CRÉER
├── types/
│   └── project.ts             # CRÉER (si n'existe pas)

Fichiers à modifier :

frontend/i18n/
├── fr.json                    # AJOUTER clés projects.*
└── en.json                    # AJOUTER clés projects.*

References

  • [Source: docs/planning-artifacts/epics.md#Story-2.1]
  • [Source: docs/planning-artifacts/architecture.md#Frontend-Architecture]
  • [Source: docs/planning-artifacts/ux-design-specification.md#Visual-Design-Foundation]
  • [Source: docs/planning-artifacts/ux-design-specification.md#Accessibility-Strategy]

Technical Requirements

Requirement Value Source
Image format WebP avec fallback NFR8
Lazy loading Native via NuxtImg NFR1, NFR2
Animations Respect prefers-reduced-motion NFR6
Accessibilité WCAG AA UX Spec
Responsive Mobile-first UX Spec

Previous Story Intelligence (Epic 1)

Patterns établis à suivre :

  • Composants feature dans app/components/feature/
  • Types TypeScript dans app/types/
  • Design tokens TailwindCSS : sky-dark, sky-accent, sky-text
  • Polices : font-ui (sans-serif), font-narrative (serif)
  • i18n via useI18n() et localePath()

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