# Story 2.6: Page Témoignages et migrations BDD Status: ready-for-dev ## 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 - [ ] **Task 1: Créer la migration table testimonials** (AC: #1) - [ ] Créer migration `create_testimonials_table` - [ ] Colonnes : id, name, role, company, avatar, text_key, personality (ENUM: sage, sarcastique, enthousiaste, professionnel), project_id (FK nullable), display_order, is_active (boolean), timestamps - [ ] Foreign key project_id → projects.id (nullable, ON DELETE SET NULL) - [ ] Index sur display_order pour le tri - [ ] Index sur is_active pour le filtrage - [ ] **Task 2: Créer le Model Testimonial** (AC: #1) - [ ] Créer `app/Models/Testimonial.php` - [ ] Définir les fillable : name, role, company, avatar, text_key, personality, project_id, display_order, is_active - [ ] Casts : is_active → boolean - [ ] Relation `project()` : belongsTo(Project::class) - [ ] Scope `scopeActive($query)` pour filtrer les actifs - [ ] Scope `scopeOrdered($query)` pour le tri - [ ] **Task 3: Créer le Seeder des témoignages** (AC: #2) - [ ] Créer `database/seeders/TestimonialSeeder.php` - [ ] Ajouter 4-5 témoignages de test avec différentes personnalités - [ ] Ajouter les traductions FR et EN dans TranslationSeeder - [ ] Lier certains témoignages à des projets existants - [ ] Mettre à jour `DatabaseSeeder.php` - [ ] **Task 4: Créer l'endpoint API testimonials** (AC: #3, #4, #6, #7) - [ ] Créer `app/Http/Controllers/Api/TestimonialController.php` - [ ] Méthode `index()` pour lister les témoignages actifs - [ ] Créer `app/Http/Resources/TestimonialResource.php` - [ ] Inclure le projet lié (si existe) avec titre traduit - [ ] Trier par display_order - [ ] Ajouter la route `GET /api/testimonials` - [ ] **Task 5: Créer le composable useFetchTestimonials** (AC: #3) - [ ] Créer `frontend/app/composables/useFetchTestimonials.ts` - [ ] Typer la réponse avec interface Testimonial[] - [ ] **Task 6: Créer la page temoignages.vue** (AC: #3, #4, #5, #8) - [ ] Créer `frontend/app/pages/temoignages.vue` - [ ] Charger les données avec le composable - [ ] Afficher chaque témoignage comme une card - [ ] Appliquer un style visuel selon la personnalité - [ ] Préparer l'emplacement pour DialoguePNJ - [ ] **Task 7: Créer le composant TestimonialCard** (AC: #4, #5, #6) - [ ] Créer `frontend/app/components/feature/TestimonialCard.vue` - [ ] Props : testimonial (avec name, role, company, avatar, text, personality, project) - [ ] Afficher l'avatar, le nom, le rôle, l'entreprise - [ ] Afficher le texte du témoignage - [ ] Style de bulle selon la personnalité - [ ] Lien vers le projet si présent - [ ] **Task 8: Styles visuels par personnalité** (AC: #5) - [ ] Définir 4 styles de bulles/cards selon personality : - sage : style calme, bordure subtile - sarcastique : style décalé, accent différent - enthousiaste : style vif, couleurs plus marquées - professionnel : style sobre, formel - [ ] Classes CSS ou Tailwind variants - [ ] **Task 9: Meta tags SEO** (AC: #9) - [ ] Titre : "Témoignages | Skycel" - [ ] Description dynamique - [ ] **Task 10: Tests et validation** - [ ] Exécuter les migrations - [ ] Vérifier le seeding des données - [ ] Tester l'API en FR et EN - [ ] Valider l'affichage de la page - [ ] 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 | ### File List