🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
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>
This commit is contained in:
@@ -0,0 +1,660 @@
|
||||
# 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
|
||||
<?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 |
|
||||
|
||||
### File List
|
||||
|
||||
Reference in New Issue
Block a user