diff --git a/api/app/Http/Controllers/Api/TestimonialController.php b/api/app/Http/Controllers/Api/TestimonialController.php new file mode 100644 index 0000000..0f0d8b0 --- /dev/null +++ b/api/app/Http/Controllers/Api/TestimonialController.php @@ -0,0 +1,21 @@ +active() + ->ordered() + ->get(); + + return TestimonialResource::collection($testimonials) + ->additional(['meta' => ['lang' => app()->getLocale()]]); + } +} diff --git a/api/app/Http/Resources/TestimonialResource.php b/api/app/Http/Resources/TestimonialResource.php new file mode 100644 index 0000000..41e7e67 --- /dev/null +++ b/api/app/Http/Resources/TestimonialResource.php @@ -0,0 +1,30 @@ + $this->id, + 'name' => $this->name, + 'role' => $this->role, + 'company' => $this->company, + 'avatar' => $this->avatar, + 'text' => $this->getTranslated('text_key'), + 'personality' => $this->personality, + 'display_order' => $this->display_order, + 'project' => $this->whenLoaded('project', function () { + return $this->project ? [ + 'id' => $this->project->id, + 'slug' => $this->project->slug, + 'title' => $this->project->getTranslated('title_key'), + ] : null; + }), + ]; + } +} diff --git a/api/app/Models/Testimonial.php b/api/app/Models/Testimonial.php new file mode 100644 index 0000000..15496fa --- /dev/null +++ b/api/app/Models/Testimonial.php @@ -0,0 +1,44 @@ + 'boolean', + ]; + + public function project(): BelongsTo + { + return $this->belongsTo(Project::class); + } + + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + + public function scopeOrdered(Builder $query): Builder + { + return $query->orderBy('display_order'); + } +} diff --git a/api/database/migrations/2026_02_06_000001_create_testimonials_table.php b/api/database/migrations/2026_02_06_000001_create_testimonials_table.php new file mode 100644 index 0000000..2e1cb0a --- /dev/null +++ b/api/database/migrations/2026_02_06_000001_create_testimonials_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/api/database/seeders/DatabaseSeeder.php b/api/database/seeders/DatabaseSeeder.php index 7659065..bca373d 100644 --- a/api/database/seeders/DatabaseSeeder.php +++ b/api/database/seeders/DatabaseSeeder.php @@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder SkillSeeder::class, ProjectSeeder::class, SkillProjectSeeder::class, + TestimonialSeeder::class, ]); } } diff --git a/api/database/seeders/TestimonialSeeder.php b/api/database/seeders/TestimonialSeeder.php new file mode 100644 index 0000000..226e643 --- /dev/null +++ b/api/database/seeders/TestimonialSeeder.php @@ -0,0 +1,95 @@ + 'Marie Dupont', + 'role' => 'CTO', + 'company' => 'TechStartup', + 'avatar' => null, + 'text_key' => 'testimonial.marie.text', + 'personality' => 'enthousiaste', + 'project_id' => 1, + 'display_order' => 1, + ], + [ + 'name' => 'Pierre Martin', + 'role' => 'Lead Developer', + 'company' => 'DevAgency', + 'avatar' => null, + 'text_key' => 'testimonial.pierre.text', + 'personality' => 'professionnel', + 'project_id' => 2, + 'display_order' => 2, + ], + [ + 'name' => 'Sophie Bernard', + 'role' => 'Product Manager', + 'company' => 'InnovateCorp', + 'avatar' => null, + 'text_key' => 'testimonial.sophie.text', + 'personality' => 'sage', + 'project_id' => null, + 'display_order' => 3, + ], + [ + 'name' => 'Thomas Leroy', + 'role' => 'Freelance Designer', + 'company' => null, + 'avatar' => null, + '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::updateOrCreate( + ['lang' => 'fr', 'key_name' => $t['key']], + ['value' => $t['fr']] + ); + Translation::updateOrCreate( + ['lang' => 'en', 'key_name' => $t['key']], + ['value' => $t['en']] + ); + } + } +} diff --git a/api/routes/api.php b/api/routes/api.php index dd5b00e..f0d540b 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Api\ProjectController; use App\Http\Controllers\Api\SkillController; +use App\Http\Controllers\Api\TestimonialController; use Illuminate\Support\Facades\Route; Route::get('/health', function () { @@ -12,3 +13,4 @@ Route::get('/projects', [ProjectController::class, 'index']); Route::get('/projects/{slug}', [ProjectController::class, 'show']); Route::get('/skills', [SkillController::class, 'index']); Route::get('/skills/{slug}/projects', [SkillController::class, 'projects']); +Route::get('/testimonials', [TestimonialController::class, 'index']); diff --git a/docs/implementation-artifacts/2-6-page-temoignages-migrations-bdd.md b/docs/implementation-artifacts/2-6-page-temoignages-migrations-bdd.md index 95eb9f6..faedd81 100644 --- a/docs/implementation-artifacts/2-6-page-temoignages-migrations-bdd.md +++ b/docs/implementation-artifacts/2-6-page-temoignages-migrations-bdd.md @@ -1,6 +1,6 @@ # Story 2.6: Page Témoignages et migrations BDD -Status: ready-for-dev +Status: review ## Story @@ -22,73 +22,73 @@ so that j'ai une validation sociale de ses compétences. ## 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 +- [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 -- [ ] **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 +- [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 -- [ ] **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` +- [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` -- [ ] **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` +- [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` -- [ ] **Task 5: Créer le composable useFetchTestimonials** (AC: #3) - - [ ] Créer `frontend/app/composables/useFetchTestimonials.ts` - - [ ] Typer la réponse avec interface Testimonial[] +- [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[] -- [ ] **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 +- [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 -- [ ] **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 +- [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 -- [ ] **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 +- [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 -- [ ] **Task 9: Meta tags SEO** (AC: #9) - - [ ] Titre : "Témoignages | Skycel" - - [ ] Description dynamique +- [x] **Task 9: Meta tags SEO** (AC: #9) + - [x] Titre : "Témoignages | Skycel" + - [x] 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 +- [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 @@ -655,6 +655,7 @@ frontend/app/ | 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 diff --git a/docs/implementation-artifacts/sprint-status.yaml b/docs/implementation-artifacts/sprint-status.yaml index 9a37cbb..2fcaf46 100644 --- a/docs/implementation-artifacts/sprint-status.yaml +++ b/docs/implementation-artifacts/sprint-status.yaml @@ -62,7 +62,7 @@ development_status: 2-3-page-projet-detail: review 2-4-page-competences-affichage-categories: review 2-5-competences-cliquables-projets-lies: review - 2-6-page-temoignages-migrations-bdd: ready-for-dev + 2-6-page-temoignages-migrations-bdd: review 2-7-composant-dialogue-pnj: ready-for-dev 2-8-page-parcours-timeline-narrative: ready-for-dev epic-2-retrospective: optional diff --git a/frontend/app/components/feature/TestimonialCard.vue b/frontend/app/components/feature/TestimonialCard.vue new file mode 100644 index 0000000..51f1d3e --- /dev/null +++ b/frontend/app/components/feature/TestimonialCard.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/frontend/app/composables/useFetchTestimonials.ts b/frontend/app/composables/useFetchTestimonials.ts new file mode 100644 index 0000000..b02abc5 --- /dev/null +++ b/frontend/app/composables/useFetchTestimonials.ts @@ -0,0 +1,15 @@ +import type { TestimonialsResponse } from '~/types/testimonial' + +export function useFetchTestimonials() { + const config = useRuntimeConfig() + const { locale } = useI18n() + + return useFetch('/testimonials', { + baseURL: config.public.apiUrl as string, + headers: { + 'X-API-Key': config.public.apiKey as string, + 'Accept-Language': locale.value, + }, + transform: (response) => response, + }) +} diff --git a/frontend/app/pages/temoignages.vue b/frontend/app/pages/temoignages.vue index 1d0e4dc..e2c0dbb 100644 --- a/frontend/app/pages/temoignages.vue +++ b/frontend/app/pages/temoignages.vue @@ -1,16 +1,128 @@ + + diff --git a/frontend/app/types/testimonial.ts b/frontend/app/types/testimonial.ts new file mode 100644 index 0000000..9fbf0ed --- /dev/null +++ b/frontend/app/types/testimonial.ts @@ -0,0 +1,22 @@ +export type PersonalityType = 'sage' | 'sarcastique' | 'enthousiaste' | 'professionnel' + +export interface Testimonial { + id: number + name: string + role: string + company: string | null + avatar: string | null + text: string + personality: PersonalityType + display_order: number + project?: { + id: number + slug: string + title: string + } | null +} + +export interface TestimonialsResponse { + data: Testimonial[] + meta: { lang: string } +} diff --git a/frontend/i18n/en.json b/frontend/i18n/en.json index b40114e..6e40774 100644 --- a/frontend/i18n/en.json +++ b/frontend/i18n/en.json @@ -108,6 +108,15 @@ "load_projects_error": "Unable to load related projects", "no_related_projects": "No projects use this skill yet" }, + "testimonials": { + "page_title": "Testimonials | Skycel", + "page_description": "Discover what my collaborators and clients say about my work.", + "load_error": "Unable to load testimonials...", + "no_testimonials": "No testimonials yet", + "cta_title": "Want to work together?", + "cta_description": "Let's discuss your project and see how I can help.", + "cta_button": "Contact me" + }, "pages": { "projects": { "title": "Projects", diff --git a/frontend/i18n/fr.json b/frontend/i18n/fr.json index 6bd259e..5526b06 100644 --- a/frontend/i18n/fr.json +++ b/frontend/i18n/fr.json @@ -108,6 +108,15 @@ "load_projects_error": "Impossible de charger les projets li\u00e9s", "no_related_projects": "Aucun projet n'utilise encore cette comp\u00e9tence" }, + "testimonials": { + "page_title": "T\u00e9moignages | Skycel", + "page_description": "D\u00e9couvrez ce que disent mes collaborateurs et clients de mon travail.", + "load_error": "Impossible de charger les t\u00e9moignages...", + "no_testimonials": "Aucun t\u00e9moignage pour le moment", + "cta_title": "Envie de travailler ensemble ?", + "cta_description": "Discutons de votre projet et voyons comment je peux vous aider.", + "cta_button": "Me contacter" + }, "pages": { "projects": { "title": "Projets",