- Add testimonials table migration with personality enum - Create Testimonial model with HasTranslations trait - Add TestimonialSeeder with 4 test testimonials - Create TestimonialController and TestimonialResource - Add useFetchTestimonials composable - Create TestimonialCard component with personality-based styling - Add temoignages.vue page with loading/error states - Add testimonials translations in FR/EN Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
662 lines
21 KiB
Markdown
662 lines
21 KiB
Markdown
# 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
|
|
<?php
|
|
// database/migrations/2026_02_04_000001_create_testimonials_table.php
|
|
|
|
use Illuminate\Database\Migrations\Migration;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
return new class extends Migration
|
|
{
|
|
public function up(): void
|
|
{
|
|
Schema::create('testimonials', function (Blueprint $table) {
|
|
$table->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
|
|
<?php
|
|
// api/app/Models/Testimonial.php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
class Testimonial extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'name',
|
|
'role',
|
|
'company',
|
|
'avatar',
|
|
'text_key',
|
|
'personality',
|
|
'project_id',
|
|
'display_order',
|
|
'is_active',
|
|
];
|
|
|
|
protected $casts = [
|
|
'is_active' => '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
|
|
<?php
|
|
// database/seeders/TestimonialSeeder.php
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Models\Testimonial;
|
|
use App\Models\Translation;
|
|
use Illuminate\Database\Seeder;
|
|
|
|
class TestimonialSeeder extends Seeder
|
|
{
|
|
public function run(): void
|
|
{
|
|
$testimonials = [
|
|
[
|
|
'name' => '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
|
|
<?php
|
|
// api/app/Http/Controllers/Api/TestimonialController.php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Resources\TestimonialResource;
|
|
use App\Models\Testimonial;
|
|
use Illuminate\Http\Request;
|
|
|
|
class TestimonialController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$lang = $request->header('Accept-Language', 'fr');
|
|
|
|
$testimonials = Testimonial::with('project')
|
|
->active()
|
|
->ordered()
|
|
->get();
|
|
|
|
return TestimonialResource::collection($testimonials)
|
|
->additional(['meta' => ['lang' => $lang]]);
|
|
}
|
|
}
|
|
```
|
|
|
|
```php
|
|
<?php
|
|
// api/app/Http/Resources/TestimonialResource.php
|
|
|
|
namespace App\Http\Resources;
|
|
|
|
use App\Models\Translation;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\JsonResource;
|
|
|
|
class TestimonialResource extends JsonResource
|
|
{
|
|
public function toArray(Request $request): array
|
|
{
|
|
$lang = $request->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
|
|
<!-- frontend/app/components/feature/TestimonialCard.vue -->
|
|
<script setup lang="ts">
|
|
import type { Testimonial } from '~/types/testimonial'
|
|
|
|
const props = defineProps<{
|
|
testimonial: Testimonial
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const localePath = useLocalePath()
|
|
|
|
// Styles selon la personnalité
|
|
const personalityStyles = {
|
|
sage: 'border-l-4 border-blue-400 bg-blue-400/5',
|
|
sarcastique: 'border-l-4 border-purple-400 bg-purple-400/5 italic',
|
|
enthousiaste: 'border-l-4 border-sky-accent bg-sky-accent/5',
|
|
professionnel: 'border-l-4 border-gray-400 bg-gray-400/5',
|
|
}
|
|
|
|
const bubbleStyle = computed(() => personalityStyles[props.testimonial.personality])
|
|
</script>
|
|
|
|
<template>
|
|
<article class="testimonial-card bg-sky-dark-50 rounded-lg overflow-hidden">
|
|
<!-- Header avec avatar et info -->
|
|
<div class="flex items-center gap-4 p-4 border-b border-sky-dark-100">
|
|
<!-- Avatar -->
|
|
<div class="w-16 h-16 rounded-full overflow-hidden bg-sky-dark flex-shrink-0">
|
|
<NuxtImg
|
|
v-if="testimonial.avatar"
|
|
:src="testimonial.avatar"
|
|
:alt="testimonial.name"
|
|
format="webp"
|
|
width="64"
|
|
height="64"
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
<div v-else class="w-full h-full flex items-center justify-center text-2xl text-sky-text-muted">
|
|
👤
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="font-ui font-semibold text-sky-text truncate">
|
|
{{ testimonial.name }}
|
|
</h3>
|
|
<p class="text-sm text-sky-text-muted">
|
|
{{ testimonial.role }}
|
|
<span v-if="testimonial.company">
|
|
@ {{ testimonial.company }}
|
|
</span>
|
|
</p>
|
|
<!-- Badge personnalité -->
|
|
<span
|
|
class="inline-block mt-1 text-xs px-2 py-0.5 rounded-full"
|
|
:class="{
|
|
'bg-blue-400/20 text-blue-300': testimonial.personality === 'sage',
|
|
'bg-purple-400/20 text-purple-300': testimonial.personality === 'sarcastique',
|
|
'bg-sky-accent/20 text-sky-accent': testimonial.personality === 'enthousiaste',
|
|
'bg-gray-400/20 text-gray-300': testimonial.personality === 'professionnel',
|
|
}"
|
|
>
|
|
{{ t(`testimonials.personality.${testimonial.personality}`) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Texte du témoignage -->
|
|
<div class="p-4" :class="bubbleStyle">
|
|
<p class="font-narrative text-sky-text leading-relaxed">
|
|
"{{ testimonial.text }}"
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Lien vers le projet -->
|
|
<div v-if="testimonial.project" class="px-4 pb-4">
|
|
<NuxtLink
|
|
:to="localePath(`/projets/${testimonial.project.slug}`)"
|
|
class="inline-flex items-center text-sm text-sky-accent hover:underline"
|
|
>
|
|
📁 {{ t('testimonials.relatedProject') }}: {{ testimonial.project.title }}
|
|
</NuxtLink>
|
|
</div>
|
|
</article>
|
|
</template>
|
|
```
|
|
|
|
### Page temoignages.vue
|
|
|
|
```vue
|
|
<!-- frontend/app/pages/temoignages.vue -->
|
|
<script setup lang="ts">
|
|
const { t } = useI18n()
|
|
const { data, pending, error, refresh } = useFetchTestimonials()
|
|
|
|
const testimonials = computed(() => data.value?.data ?? [])
|
|
|
|
// SEO
|
|
useHead({
|
|
title: () => t('testimonials.pageTitle'),
|
|
})
|
|
|
|
useSeoMeta({
|
|
title: () => t('testimonials.pageTitle'),
|
|
description: () => t('testimonials.pageDescription'),
|
|
ogTitle: () => t('testimonials.pageTitle'),
|
|
ogDescription: () => t('testimonials.pageDescription'),
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="container mx-auto px-4 py-8">
|
|
<h1 class="text-3xl font-ui font-bold text-sky-text mb-8">
|
|
{{ t('testimonials.title') }}
|
|
</h1>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="pending" class="space-y-6">
|
|
<div v-for="i in 4" :key="i" class="bg-sky-dark-50 rounded-lg p-6 animate-pulse">
|
|
<div class="flex items-center gap-4 mb-4">
|
|
<div class="w-16 h-16 bg-sky-dark-100 rounded-full"></div>
|
|
<div class="flex-1">
|
|
<div class="h-5 bg-sky-dark-100 rounded w-32 mb-2"></div>
|
|
<div class="h-4 bg-sky-dark-100 rounded w-48"></div>
|
|
</div>
|
|
</div>
|
|
<div class="h-4 bg-sky-dark-100 rounded w-full mb-2"></div>
|
|
<div class="h-4 bg-sky-dark-100 rounded w-3/4"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div v-else-if="error" class="text-center py-12">
|
|
<p class="text-sky-text-muted mb-4">{{ t('testimonials.loadError') }}</p>
|
|
<button
|
|
@click="refresh()"
|
|
class="bg-sky-accent text-white px-6 py-2 rounded-lg hover:bg-sky-accent-hover"
|
|
>
|
|
{{ t('common.retry') }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Testimonials list -->
|
|
<div v-else class="space-y-6">
|
|
<!-- Placeholder pour DialoguePNJ (Story 2.7) -->
|
|
<div class="hidden">
|
|
<!-- <DialoguePNJ :testimonials="testimonials" /> -->
|
|
</div>
|
|
|
|
<!-- Affichage en cards (sera remplacé par DialoguePNJ) -->
|
|
<TestimonialCard
|
|
v-for="testimonial in testimonials"
|
|
:key="testimonial.id"
|
|
:testimonial="testimonial"
|
|
class="testimonial-animated"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.testimonial-animated {
|
|
animation: fadeInUp 0.5s ease-out forwards;
|
|
opacity: 0;
|
|
}
|
|
|
|
.testimonial-animated:nth-child(1) { animation-delay: 0ms; }
|
|
.testimonial-animated:nth-child(2) { animation-delay: 100ms; }
|
|
.testimonial-animated:nth-child(3) { animation-delay: 200ms; }
|
|
.testimonial-animated:nth-child(4) { animation-delay: 300ms; }
|
|
.testimonial-animated:nth-child(5) { animation-delay: 400ms; }
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.testimonial-animated {
|
|
animation: none;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
### 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
|
|
|