# 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.*)

