✨ Add testimonials page with personality-styled cards (Story 2.6)
- 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>
This commit is contained in:
21
api/app/Http/Controllers/Api/TestimonialController.php
Normal file
21
api/app/Http/Controllers/Api/TestimonialController.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\TestimonialResource;
|
||||
use App\Models\Testimonial;
|
||||
|
||||
class TestimonialController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$testimonials = Testimonial::with('project')
|
||||
->active()
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
return TestimonialResource::collection($testimonials)
|
||||
->additional(['meta' => ['lang' => app()->getLocale()]]);
|
||||
}
|
||||
}
|
||||
30
api/app/Http/Resources/TestimonialResource.php
Normal file
30
api/app/Http/Resources/TestimonialResource.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TestimonialResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $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;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
44
api/app/Models/Testimonial.php
Normal file
44
api/app/Models/Testimonial.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Testimonial extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
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(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('display_order');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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');
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
|
||||
SkillSeeder::class,
|
||||
ProjectSeeder::class,
|
||||
SkillProjectSeeder::class,
|
||||
TestimonialSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
95
api/database/seeders/TestimonialSeeder.php
Normal file
95
api/database/seeders/TestimonialSeeder.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?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' => 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']]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user