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>
8.3 KiB
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
- Given le composant
ProjectCardest implémenté When il reçoit les données d'un projet en props Then il affiche l'image du projet (WebP, lazy loading) - And il affiche le titre traduit selon la langue courante
- And il affiche la description courte traduite
- And un hover effect révèle un CTA "Découvrir" avec animation subtile
- And le composant est cliquable et navigue vers
/projets/{slug}(ou/en/projects/{slug}) - And le composant respecte
prefers-reduced-motionpour les animations - And le composant est responsive (adaptation mobile/desktop)
- And le composant est accessible (focus visible,
roleapproprié)
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)
- Créer le fichier
-
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)
- Rendre la card entièrement cliquable avec
-
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
- Créer une media query CSS pour détecter
-
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 containeraltdescriptif 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()etlocalePath()
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 |