# Story 2.6: Page Témoignages et migrations BDD Status: review ## Story As a visiteur, I want voir les témoignages des personnes ayant travaillé avec le développeur, so that j'ai une validation sociale de ses compétences. ## Acceptance Criteria 1. **Given** les migrations Laravel sont exécutées **When** `php artisan migrate` est lancé **Then** la table `testimonials` est créée (id, name, role, company, avatar, text_key, personality ENUM, project_id FK nullable, display_order, is_active, timestamps) 2. **And** les seeders de test sont disponibles avec des témoignages en FR et EN 3. **Given** le visiteur accède à `/temoignages` (FR) ou `/en/testimonials` (EN) **When** la page se charge **Then** la liste des témoignages s'affiche depuis l'API `/api/testimonials` 4. **And** chaque témoignage affiche : nom, rôle, entreprise, avatar, texte traduit 5. **And** la personnalité de chaque PNJ est indiquée visuellement (style différent selon personality) 6. **And** un lien vers le projet associé est présent si pertinent 7. **And** l'ordre d'affichage respecte `display_order` 8. **And** le design est préparé pour accueillir le composant DialoguePNJ (story suivante) 9. **And** les meta tags SEO sont dynamiques pour cette page ## Tasks / Subtasks - [x] **Task 1: Créer la migration table testimonials** (AC: #1) - [x] Créer migration `create_testimonials_table` - [x] Colonnes : id, name, role, company, avatar, text_key, personality (ENUM: sage, sarcastique, enthousiaste, professionnel), project_id (FK nullable), display_order, is_active (boolean), timestamps - [x] Foreign key project_id → projects.id (nullable, ON DELETE SET NULL) - [x] Index sur display_order pour le tri - [x] Index sur is_active pour le filtrage - [x] **Task 2: Créer le Model Testimonial** (AC: #1) - [x] Créer `app/Models/Testimonial.php` - [x] Définir les fillable : name, role, company, avatar, text_key, personality, project_id, display_order, is_active - [x] Casts : is_active → boolean - [x] Relation `project()` : belongsTo(Project::class) - [x] Scope `scopeActive($query)` pour filtrer les actifs - [x] Scope `scopeOrdered($query)` pour le tri - [x] **Task 3: Créer le Seeder des témoignages** (AC: #2) - [x] Créer `database/seeders/TestimonialSeeder.php` - [x] Ajouter 4-5 témoignages de test avec différentes personnalités - [x] Ajouter les traductions FR et EN dans TranslationSeeder - [x] Lier certains témoignages à des projets existants - [x] Mettre à jour `DatabaseSeeder.php` - [x] **Task 4: Créer l'endpoint API testimonials** (AC: #3, #4, #6, #7) - [x] Créer `app/Http/Controllers/Api/TestimonialController.php` - [x] Méthode `index()` pour lister les témoignages actifs - [x] Créer `app/Http/Resources/TestimonialResource.php` - [x] Inclure le projet lié (si existe) avec titre traduit - [x] Trier par display_order - [x] Ajouter la route `GET /api/testimonials` - [x] **Task 5: Créer le composable useFetchTestimonials** (AC: #3) - [x] Créer `frontend/app/composables/useFetchTestimonials.ts` - [x] Typer la réponse avec interface Testimonial[] - [x] **Task 6: Créer la page temoignages.vue** (AC: #3, #4, #5, #8) - [x] Créer `frontend/app/pages/temoignages.vue` - [x] Charger les données avec le composable - [x] Afficher chaque témoignage comme une card - [x] Appliquer un style visuel selon la personnalité - [x] Préparer l'emplacement pour DialoguePNJ - [x] **Task 7: Créer le composant TestimonialCard** (AC: #4, #5, #6) - [x] Créer `frontend/app/components/feature/TestimonialCard.vue` - [x] Props : testimonial (avec name, role, company, avatar, text, personality, project) - [x] Afficher l'avatar, le nom, le rôle, l'entreprise - [x] Afficher le texte du témoignage - [x] Style de bulle selon la personnalité - [x] Lien vers le projet si présent - [x] **Task 8: Styles visuels par personnalité** (AC: #5) - [x] Définir 4 styles de bulles/cards selon personality : - sage : style calme, bordure subtile (emerald) - sarcastique : style décalé, accent différent (purple) - enthousiaste : style vif, couleurs plus marquées (amber) - professionnel : style sobre, formel (sky) - [x] Classes CSS ou Tailwind variants - [x] **Task 9: Meta tags SEO** (AC: #9) - [x] Titre : "Témoignages | Skycel" - [x] Description dynamique - [x] **Task 10: Tests et validation** - [x] Exécuter les migrations - [x] Vérifier le seeding des données - [x] Tester l'API en FR et EN - [x] Valider l'affichage de la page - [x] Vérifier les liens vers projets ## Dev Notes ### Migration testimonials ```php id(); $table->string('name'); $table->string('role'); $table->string('company')->nullable(); $table->string('avatar')->nullable(); $table->string('text_key'); $table->enum('personality', ['sage', 'sarcastique', 'enthousiaste', 'professionnel'])->default('professionnel'); $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete(); $table->integer('display_order')->default(0); $table->boolean('is_active')->default(true); $table->timestamps(); $table->index('display_order'); $table->index('is_active'); }); } public function down(): void { Schema::dropIfExists('testimonials'); } }; ``` ### Model Testimonial ```php 'boolean', ]; public function project(): BelongsTo { return $this->belongsTo(Project::class); } public function scopeActive($query) { return $query->where('is_active', true); } public function scopeOrdered($query) { return $query->orderBy('display_order'); } } ``` ### Seeder des témoignages ```php 'Marie Dupont', 'role' => 'CTO', 'company' => 'TechStartup', 'avatar' => '/images/testimonials/marie.jpg', 'text_key' => 'testimonial.marie.text', 'personality' => 'enthousiaste', 'project_id' => 1, 'display_order' => 1, ], [ 'name' => 'Pierre Martin', 'role' => 'Lead Developer', 'company' => 'DevAgency', 'avatar' => '/images/testimonials/pierre.jpg', 'text_key' => 'testimonial.pierre.text', 'personality' => 'professionnel', 'project_id' => 2, 'display_order' => 2, ], [ 'name' => 'Sophie Bernard', 'role' => 'Product Manager', 'company' => 'InnovateCorp', 'avatar' => '/images/testimonials/sophie.jpg', 'text_key' => 'testimonial.sophie.text', 'personality' => 'sage', 'project_id' => null, 'display_order' => 3, ], [ 'name' => 'Thomas Leroy', 'role' => 'Freelance Designer', 'company' => null, 'avatar' => '/images/testimonials/thomas.jpg', 'text_key' => 'testimonial.thomas.text', 'personality' => 'sarcastique', 'project_id' => null, 'display_order' => 4, ], ]; foreach ($testimonials as $data) { Testimonial::create($data); } // Traductions $translations = [ ['key' => 'testimonial.marie.text', 'fr' => "Travailler avec Célian a été une révélation ! Son approche créative et sa maîtrise technique ont transformé notre projet. Je recommande sans hésitation !", 'en' => "Working with Célian was a revelation! His creative approach and technical mastery transformed our project. I highly recommend!"], ['key' => 'testimonial.pierre.text', 'fr' => "Code propre, architecture solide, communication claire. Célian sait exactement ce qu'il fait et le fait bien.", 'en' => "Clean code, solid architecture, clear communication. Célian knows exactly what he's doing and does it well."], ['key' => 'testimonial.sophie.text', 'fr' => "Une personne rare qui combine vision produit et excellence technique. Les retours utilisateurs parlent d'eux-mêmes.", 'en' => "A rare person who combines product vision and technical excellence. User feedback speaks for itself."], ['key' => 'testimonial.thomas.text', 'fr' => "Bon, j'avoue, au début je pensais que les devs ne comprenaient rien au design. Célian m'a prouvé le contraire. Presque agaçant.", 'en' => "Okay, I admit, at first I thought devs didn't understand design. Célian proved me wrong. Almost annoying."], ]; foreach ($translations as $t) { Translation::create(['lang' => 'fr', 'key_name' => $t['key'], 'value' => $t['fr']]); Translation::create(['lang' => 'en', 'key_name' => $t['key'], 'value' => $t['en']]); } } } ``` ### Controller et Resource ```php header('Accept-Language', 'fr'); $testimonials = Testimonial::with('project') ->active() ->ordered() ->get(); return TestimonialResource::collection($testimonials) ->additional(['meta' => ['lang' => $lang]]); } } ``` ```php header('Accept-Language', 'fr'); return [ 'id' => $this->id, 'name' => $this->name, 'role' => $this->role, 'company' => $this->company, 'avatar' => $this->avatar, 'text' => Translation::getTranslation($this->text_key, $lang), 'personality' => $this->personality, 'displayOrder' => $this->display_order, 'project' => $this->whenLoaded('project', function () use ($lang) { return $this->project ? [ 'id' => $this->project->id, 'slug' => $this->project->slug, 'title' => Translation::getTranslation($this->project->title_key, $lang), ] : null; }), ]; } } ``` ```php // api/routes/api.php Route::get('/testimonials', [TestimonialController::class, 'index']); ``` ### Types TypeScript ```typescript // frontend/app/types/testimonial.ts export interface Testimonial { id: number name: string role: string company: string | null avatar: string | null text: string personality: 'sage' | 'sarcastique' | 'enthousiaste' | 'professionnel' displayOrder: number project?: { id: number slug: string title: string } | null } ``` ### Composant TestimonialCard ```vue ``` ### Page temoignages.vue ```vue ``` ### Clés i18n **fr.json :** ```json { "testimonials": { "title": "Témoignages", "pageTitle": "Témoignages | Skycel", "pageDescription": "Découvrez ce que disent les personnes qui ont travaillé avec Célian.", "loadError": "Impossible de charger les témoignages...", "relatedProject": "Projet associé", "personality": { "sage": "Sage", "sarcastique": "Sarcastique", "enthousiaste": "Enthousiaste", "professionnel": "Professionnel" } } } ``` **en.json :** ```json { "testimonials": { "title": "Testimonials", "pageTitle": "Testimonials | Skycel", "pageDescription": "Discover what people who worked with Célian have to say.", "loadError": "Unable to load testimonials...", "relatedProject": "Related project", "personality": { "sage": "Wise", "sarcastique": "Sarcastic", "enthousiaste": "Enthusiastic", "professionnel": "Professional" } } } ``` ### Dépendances **Cette story nécessite :** - Story 1.2 : Table projects pour la FK - Story 1.3 : Système i18n configuré **Cette story prépare pour :** - Story 2.7 : Composant DialoguePNJ ### Project Structure Notes **Fichiers à créer :** ``` api/ ├── app/Models/ │ └── Testimonial.php # CRÉER ├── app/Http/Controllers/Api/ │ └── TestimonialController.php # CRÉER ├── app/Http/Resources/ │ └── TestimonialResource.php # CRÉER └── database/ ├── migrations/ │ └── 2026_02_04_000001_create_testimonials_table.php # CRÉER └── seeders/ └── TestimonialSeeder.php # CRÉER frontend/app/ ├── pages/ │ └── temoignages.vue # CRÉER ├── components/feature/ │ └── TestimonialCard.vue # CRÉER ├── composables/ │ └── useFetchTestimonials.ts # CRÉER └── types/ └── testimonial.ts # CRÉER ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-2.6] - [Source: docs/planning-artifacts/architecture.md#API-&-Communication-Patterns] - [Source: docs/planning-artifacts/ux-design-specification.md#DialoguePNJ] - [Source: docs/brainstorming-gamification-2026-01-26.md#Personnalites-PNJ] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | Table | testimonials avec personality ENUM | Epics | | API endpoint | GET /api/testimonials | Architecture | | Personnalités | sage, sarcastique, enthousiaste, professionnel | Brainstorming | | FK project_id | Nullable, ON DELETE SET NULL | Architecture | ## 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 | | 2026-02-06 | Implémentation complète: migration, model, seeder, API, frontend | Claude Opus 4.5 | ### File List