Files
Portfolio-Game/docs/implementation-artifacts/2-1-composant-projectcard.md
skycel 4117a84809 🎨 Add ProjectCard component with hover effect (Story 2.1)
Reusable project card with NuxtImg lazy loading, hover overlay with
"Discover" CTA, responsive design, and full accessibility support
including prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 02:04:24 +01:00

304 lines
9.3 KiB
Markdown

# Story 2.1: Composant ProjectCard
Status: review
## 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
- [x] **Task 1: Créer le composant ProjectCard.vue** (AC: #1, #2, #3)
- [x] Créer le fichier `frontend/app/components/feature/ProjectCard.vue`
- [x] Définir les props TypeScript : `project` (object avec slug, image, title, shortDescription)
- [x] Utiliser `<NuxtImg>` pour l'image avec format WebP et lazy loading
- [x] Intégrer `useI18n()` pour le titre et la description traduits
- [x] Afficher titre (`project.title`) et description courte (`project.shortDescription`)
- [x] **Task 2: Implémenter le hover effect et CTA** (AC: #4)
- [x] Créer un overlay qui apparaît au hover avec transition CSS
- [x] Ajouter un CTA "Découvrir" (traduit via i18n) centré dans l'overlay
- [x] Animation subtile : fade-in + léger scale (0.98 → 1)
- [x] Utiliser les classes Tailwind pour les transitions
- [x] **Task 3: Implémenter la navigation** (AC: #5)
- [x] Rendre la card entièrement cliquable avec `<NuxtLink>`
- [x] Utiliser `localePath()` pour générer l'URL correcte selon la langue
- [x] URL pattern : `/projets/{slug}` (FR) ou `/en/projects/{slug}` (EN)
- [x] **Task 4: Gérer `prefers-reduced-motion`** (AC: #6)
- [x] Créer une media query CSS pour détecter `prefers-reduced-motion: reduce`
- [x] Désactiver les transitions et animations si motion réduite
- [x] Le hover effect reste visible mais sans animation
- [x] **Task 5: Rendre le composant responsive** (AC: #7)
- [x] Mobile : card pleine largeur, hauteur fixe ou aspect-ratio
- [x] Desktop : card avec largeur flexible pour grille (min 280px, max 400px)
- [x] Image qui remplit la card avec `object-cover`
- [x] Texte tronqué si trop long (ellipsis)
- [x] **Task 6: Accessibilité** (AC: #8)
- [x] Focus visible sur la card (outline accent)
- [x] `role="article"` sur la card container
- [x] `alt` descriptif sur l'image (utiliser le titre du projet)
- [x] Navigation au clavier fonctionnelle (Tab, Enter)
- [x] **Task 7: Tests et validation**
- [x] Tester le composant avec des données de projet fictives
- [x] Vérifier l'affichage en FR et EN
- [x] Vérifier le hover effect et la navigation
- [x] Tester sur mobile et desktop
- [x] Valider l'accessibilité avec axe DevTools
## Dev Notes
### Structure du composant
```vue
<!-- 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
```typescript
// 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` :
```json
{
"projects": {
"discover": "Découvrir"
}
}
```
```json
{
"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
Claude Opus 4.5 (claude-opus-4-5-20251101)
### Debug Log References
- Aucun problème rencontré. Build et types validés.
### Completion Notes List
- Type Project créé dans `types/project.ts` avec interface complète (match API snake_case)
- Composant ProjectCard avec NuxtImg (WebP, lazy loading), hover overlay, CTA "Découvrir"
- Navigation via NuxtLink + localePath vers `/projets/{slug}`
- Responsive : hauteur image 176px mobile, 192px desktop
- Accessibilité : role="article", alt sur image, focus-visible outline
- prefers-reduced-motion : toutes animations désactivées
- Placeholder image si `project.image` absent
- Border hover effect (sky-accent/30)
- Traductions projects.discover, no_projects, view_all ajoutées
### Change Log
| Date | Change | Author |
|------|--------|--------|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |
| 2026-02-06 | Tasks 1-7 implémentées et validées | Dev Agent (Claude Opus 4.5) |
### File List
- `frontend/app/types/project.ts` — CRÉÉ
- `frontend/app/components/feature/ProjectCard.vue` — CRÉÉ
- `frontend/i18n/fr.json` — MODIFIÉ (ajout projects.*)
- `frontend/i18n/en.json` — MODIFIÉ (ajout projects.*)