🗃️ Add database schema, models & seeders (Story 1.2)
Migrations (translations, projects, skills, skill_project), Eloquent models with belongsToMany relations, scopes, and test seeders (74 translations FR/EN, 10 skills, 3 projects, 12 skill-project links). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
48
api/app/Models/Project.php
Normal file
48
api/app/Models/Project.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
class Project extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'slug',
|
||||||
|
'title_key',
|
||||||
|
'description_key',
|
||||||
|
'short_description_key',
|
||||||
|
'image',
|
||||||
|
'url',
|
||||||
|
'github_url',
|
||||||
|
'date_completed',
|
||||||
|
'is_featured',
|
||||||
|
'display_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date_completed' => 'date',
|
||||||
|
'is_featured' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function skills(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Skill::class, 'skill_project')
|
||||||
|
->withPivot(['level_before', 'level_after', 'level_description_key'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeFeatured(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_featured', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeOrdered(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->orderBy('display_order');
|
||||||
|
}
|
||||||
|
}
|
||||||
37
api/app/Models/Skill.php
Normal file
37
api/app/Models/Skill.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
class Skill extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'slug',
|
||||||
|
'name_key',
|
||||||
|
'description_key',
|
||||||
|
'icon',
|
||||||
|
'category',
|
||||||
|
'max_level',
|
||||||
|
'display_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function projects(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Project::class, 'skill_project')
|
||||||
|
->withPivot(['level_before', 'level_after', 'level_description_key'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeByCategory(Builder $query, string $category): Builder
|
||||||
|
{
|
||||||
|
return $query->where('category', $category);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeOrdered(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->orderBy('display_order');
|
||||||
|
}
|
||||||
|
}
|
||||||
27
api/app/Models/Translation.php
Normal file
27
api/app/Models/Translation.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class Translation extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['lang', 'key_name', 'value'];
|
||||||
|
|
||||||
|
public function scopeForLang(Builder $query, string $lang): Builder
|
||||||
|
{
|
||||||
|
return $query->where('lang', $lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTranslation(string $key, string $lang, string $fallback = 'fr'): ?string
|
||||||
|
{
|
||||||
|
$translation = static::where('key_name', $key)->where('lang', $lang)->first();
|
||||||
|
|
||||||
|
if (!$translation && $lang !== $fallback) {
|
||||||
|
$translation = static::where('key_name', $key)->where('lang', $fallback)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $translation?->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?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('translations', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('lang', 5);
|
||||||
|
$table->string('key_name', 255);
|
||||||
|
$table->text('value');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['lang', 'key_name'], 'unique_translation');
|
||||||
|
$table->index('lang', 'idx_lang');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('translations');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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('projects', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->string('title_key');
|
||||||
|
$table->string('description_key');
|
||||||
|
$table->string('short_description_key');
|
||||||
|
$table->string('image')->nullable();
|
||||||
|
$table->string('url')->nullable();
|
||||||
|
$table->string('github_url')->nullable();
|
||||||
|
$table->date('date_completed');
|
||||||
|
$table->boolean('is_featured')->default(false);
|
||||||
|
$table->integer('display_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('display_order');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('projects');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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('skills', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->string('name_key');
|
||||||
|
$table->string('description_key');
|
||||||
|
$table->string('icon')->nullable();
|
||||||
|
$table->string('category');
|
||||||
|
$table->integer('max_level')->default(5);
|
||||||
|
$table->integer('display_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('category');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('skills');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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('skill_project', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('skill_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->integer('level_before');
|
||||||
|
$table->integer('level_after');
|
||||||
|
$table->string('level_description_key')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['skill_id', 'project_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('skill_project');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
@@ -10,16 +9,13 @@ class DatabaseSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
use WithoutModelEvents;
|
use WithoutModelEvents;
|
||||||
|
|
||||||
/**
|
|
||||||
* Seed the application's database.
|
|
||||||
*/
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
$this->call([
|
||||||
|
TranslationSeeder::class,
|
||||||
User::factory()->create([
|
SkillSeeder::class,
|
||||||
'name' => 'Test User',
|
ProjectSeeder::class,
|
||||||
'email' => 'test@example.com',
|
SkillProjectSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
api/database/seeders/ProjectSeeder.php
Normal file
55
api/database/seeders/ProjectSeeder.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class ProjectSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$projects = [
|
||||||
|
[
|
||||||
|
'slug' => 'skycel-portfolio',
|
||||||
|
'title_key' => 'project.skycel-portfolio.title',
|
||||||
|
'description_key' => 'project.skycel-portfolio.description',
|
||||||
|
'short_description_key' => 'project.skycel-portfolio.short_description',
|
||||||
|
'image' => 'projects/skycel-portfolio.png',
|
||||||
|
'url' => 'https://skycel.dev',
|
||||||
|
'github_url' => 'https://git.araneite.dev/skycel/Portfolio-Game',
|
||||||
|
'date_completed' => '2026-03-01',
|
||||||
|
'is_featured' => true,
|
||||||
|
'display_order' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'slug' => 'ecommerce-app',
|
||||||
|
'title_key' => 'project.ecommerce-app.title',
|
||||||
|
'description_key' => 'project.ecommerce-app.description',
|
||||||
|
'short_description_key' => 'project.ecommerce-app.short_description',
|
||||||
|
'image' => 'projects/ecommerce-app.png',
|
||||||
|
'url' => null,
|
||||||
|
'github_url' => 'https://github.com/skycel/ecommerce-app',
|
||||||
|
'date_completed' => '2025-09-15',
|
||||||
|
'is_featured' => true,
|
||||||
|
'display_order' => 2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'slug' => 'analytics-dashboard',
|
||||||
|
'title_key' => 'project.analytics-dashboard.title',
|
||||||
|
'description_key' => 'project.analytics-dashboard.description',
|
||||||
|
'short_description_key' => 'project.analytics-dashboard.short_description',
|
||||||
|
'image' => 'projects/analytics-dashboard.png',
|
||||||
|
'url' => null,
|
||||||
|
'github_url' => null,
|
||||||
|
'date_completed' => '2025-06-01',
|
||||||
|
'is_featured' => false,
|
||||||
|
'display_order' => 3,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($projects as $project) {
|
||||||
|
Project::create($project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
api/database/seeders/SkillProjectSeeder.php
Normal file
83
api/database/seeders/SkillProjectSeeder.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Skill;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class SkillProjectSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$skycel = Project::where('slug', 'skycel-portfolio')->first();
|
||||||
|
$ecommerce = Project::where('slug', 'ecommerce-app')->first();
|
||||||
|
$dashboard = Project::where('slug', 'analytics-dashboard')->first();
|
||||||
|
|
||||||
|
// Skycel Portfolio: Vue.js, Nuxt, Laravel, TailwindCSS, Git
|
||||||
|
$skycel->skills()->attach(Skill::where('slug', 'vuejs')->first()->id, [
|
||||||
|
'level_before' => 2,
|
||||||
|
'level_after' => 4,
|
||||||
|
'level_description_key' => 'skill_project.skycel-portfolio.vuejs.level_description',
|
||||||
|
]);
|
||||||
|
$skycel->skills()->attach(Skill::where('slug', 'nuxt')->first()->id, [
|
||||||
|
'level_before' => 0,
|
||||||
|
'level_after' => 3,
|
||||||
|
'level_description_key' => 'skill_project.skycel-portfolio.nuxt.level_description',
|
||||||
|
]);
|
||||||
|
$skycel->skills()->attach(Skill::where('slug', 'laravel')->first()->id, [
|
||||||
|
'level_before' => 2,
|
||||||
|
'level_after' => 3,
|
||||||
|
'level_description_key' => 'skill_project.skycel-portfolio.laravel.level_description',
|
||||||
|
]);
|
||||||
|
$skycel->skills()->attach(Skill::where('slug', 'tailwindcss')->first()->id, [
|
||||||
|
'level_before' => 1,
|
||||||
|
'level_after' => 3,
|
||||||
|
'level_description_key' => 'skill_project.skycel-portfolio.tailwindcss.level_description',
|
||||||
|
]);
|
||||||
|
$skycel->skills()->attach(Skill::where('slug', 'git')->first()->id, [
|
||||||
|
'level_before' => 3,
|
||||||
|
'level_after' => 4,
|
||||||
|
'level_description_key' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// E-commerce App: Laravel, Vue.js, PHP, Docker
|
||||||
|
$ecommerce->skills()->attach(Skill::where('slug', 'laravel')->first()->id, [
|
||||||
|
'level_before' => 1,
|
||||||
|
'level_after' => 2,
|
||||||
|
'level_description_key' => 'skill_project.ecommerce-app.laravel.level_description',
|
||||||
|
]);
|
||||||
|
$ecommerce->skills()->attach(Skill::where('slug', 'vuejs')->first()->id, [
|
||||||
|
'level_before' => 1,
|
||||||
|
'level_after' => 2,
|
||||||
|
'level_description_key' => 'skill_project.ecommerce-app.vuejs.level_description',
|
||||||
|
]);
|
||||||
|
$ecommerce->skills()->attach(Skill::where('slug', 'php')->first()->id, [
|
||||||
|
'level_before' => 2,
|
||||||
|
'level_after' => 3,
|
||||||
|
'level_description_key' => null,
|
||||||
|
]);
|
||||||
|
$ecommerce->skills()->attach(Skill::where('slug', 'docker')->first()->id, [
|
||||||
|
'level_before' => 0,
|
||||||
|
'level_after' => 1,
|
||||||
|
'level_description_key' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Analytics Dashboard: TypeScript, Node.js, Git
|
||||||
|
$dashboard->skills()->attach(Skill::where('slug', 'typescript')->first()->id, [
|
||||||
|
'level_before' => 1,
|
||||||
|
'level_after' => 3,
|
||||||
|
'level_description_key' => 'skill_project.analytics-dashboard.typescript.level_description',
|
||||||
|
]);
|
||||||
|
$dashboard->skills()->attach(Skill::where('slug', 'nodejs')->first()->id, [
|
||||||
|
'level_before' => 1,
|
||||||
|
'level_after' => 2,
|
||||||
|
'level_description_key' => 'skill_project.analytics-dashboard.nodejs.level_description',
|
||||||
|
]);
|
||||||
|
$dashboard->skills()->attach(Skill::where('slug', 'git')->first()->id, [
|
||||||
|
'level_before' => 2,
|
||||||
|
'level_after' => 3,
|
||||||
|
'level_description_key' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
api/database/seeders/SkillSeeder.php
Normal file
36
api/database/seeders/SkillSeeder.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Skill;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class SkillSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$skills = [
|
||||||
|
// Frontend
|
||||||
|
['slug' => 'vuejs', 'name_key' => 'skill.vuejs.name', 'description_key' => 'skill.vuejs.description', 'icon' => 'vuejs', 'category' => 'Frontend', 'max_level' => 5, 'display_order' => 1],
|
||||||
|
['slug' => 'nuxt', 'name_key' => 'skill.nuxt.name', 'description_key' => 'skill.nuxt.description', 'icon' => 'nuxt', 'category' => 'Frontend', 'max_level' => 5, 'display_order' => 2],
|
||||||
|
['slug' => 'typescript', 'name_key' => 'skill.typescript.name', 'description_key' => 'skill.typescript.description', 'icon' => 'typescript', 'category' => 'Frontend', 'max_level' => 5, 'display_order' => 3],
|
||||||
|
['slug' => 'tailwindcss', 'name_key' => 'skill.tailwindcss.name', 'description_key' => 'skill.tailwindcss.description', 'icon' => 'tailwindcss', 'category' => 'Frontend', 'max_level' => 5, 'display_order' => 4],
|
||||||
|
|
||||||
|
// Backend
|
||||||
|
['slug' => 'laravel', 'name_key' => 'skill.laravel.name', 'description_key' => 'skill.laravel.description', 'icon' => 'laravel', 'category' => 'Backend', 'max_level' => 5, 'display_order' => 5],
|
||||||
|
['slug' => 'php', 'name_key' => 'skill.php.name', 'description_key' => 'skill.php.description', 'icon' => 'php', 'category' => 'Backend', 'max_level' => 5, 'display_order' => 6],
|
||||||
|
['slug' => 'nodejs', 'name_key' => 'skill.nodejs.name', 'description_key' => 'skill.nodejs.description', 'icon' => 'nodejs', 'category' => 'Backend', 'max_level' => 5, 'display_order' => 7],
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
['slug' => 'git', 'name_key' => 'skill.git.name', 'description_key' => 'skill.git.description', 'icon' => 'git', 'category' => 'Tools', 'max_level' => 5, 'display_order' => 8],
|
||||||
|
['slug' => 'docker', 'name_key' => 'skill.docker.name', 'description_key' => 'skill.docker.description', 'icon' => 'docker', 'category' => 'Tools', 'max_level' => 5, 'display_order' => 9],
|
||||||
|
|
||||||
|
// Soft skills
|
||||||
|
['slug' => 'communication', 'name_key' => 'skill.communication.name', 'description_key' => 'skill.communication.description', 'icon' => 'communication', 'category' => 'Soft skills', 'max_level' => 5, 'display_order' => 10],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($skills as $skill) {
|
||||||
|
Skill::create($skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
api/database/seeders/TranslationSeeder.php
Normal file
124
api/database/seeders/TranslationSeeder.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Translation;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class TranslationSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$translations = [
|
||||||
|
// Skills - Vue.js
|
||||||
|
['fr', 'skill.vuejs.name', 'Vue.js'],
|
||||||
|
['en', 'skill.vuejs.name', 'Vue.js'],
|
||||||
|
['fr', 'skill.vuejs.description', 'Framework JavaScript progressif pour construire des interfaces utilisateur'],
|
||||||
|
['en', 'skill.vuejs.description', 'Progressive JavaScript framework for building user interfaces'],
|
||||||
|
|
||||||
|
// Skills - Nuxt
|
||||||
|
['fr', 'skill.nuxt.name', 'Nuxt'],
|
||||||
|
['en', 'skill.nuxt.name', 'Nuxt'],
|
||||||
|
['fr', 'skill.nuxt.description', 'Framework fullstack basé sur Vue.js avec rendu côté serveur'],
|
||||||
|
['en', 'skill.nuxt.description', 'Fullstack framework based on Vue.js with server-side rendering'],
|
||||||
|
|
||||||
|
// Skills - TypeScript
|
||||||
|
['fr', 'skill.typescript.name', 'TypeScript'],
|
||||||
|
['en', 'skill.typescript.name', 'TypeScript'],
|
||||||
|
['fr', 'skill.typescript.description', 'Surensemble typé de JavaScript pour un code plus robuste'],
|
||||||
|
['en', 'skill.typescript.description', 'Typed superset of JavaScript for more robust code'],
|
||||||
|
|
||||||
|
// Skills - TailwindCSS
|
||||||
|
['fr', 'skill.tailwindcss.name', 'TailwindCSS'],
|
||||||
|
['en', 'skill.tailwindcss.name', 'TailwindCSS'],
|
||||||
|
['fr', 'skill.tailwindcss.description', 'Framework CSS utilitaire pour un design rapide et personnalisable'],
|
||||||
|
['en', 'skill.tailwindcss.description', 'Utility-first CSS framework for rapid and customizable design'],
|
||||||
|
|
||||||
|
// Skills - Laravel
|
||||||
|
['fr', 'skill.laravel.name', 'Laravel'],
|
||||||
|
['en', 'skill.laravel.name', 'Laravel'],
|
||||||
|
['fr', 'skill.laravel.description', 'Framework PHP élégant pour le développement web'],
|
||||||
|
['en', 'skill.laravel.description', 'Elegant PHP framework for web development'],
|
||||||
|
|
||||||
|
// Skills - PHP
|
||||||
|
['fr', 'skill.php.name', 'PHP'],
|
||||||
|
['en', 'skill.php.name', 'PHP'],
|
||||||
|
['fr', 'skill.php.description', 'Langage de programmation côté serveur largement utilisé'],
|
||||||
|
['en', 'skill.php.description', 'Widely used server-side programming language'],
|
||||||
|
|
||||||
|
// Skills - Node.js
|
||||||
|
['fr', 'skill.nodejs.name', 'Node.js'],
|
||||||
|
['en', 'skill.nodejs.name', 'Node.js'],
|
||||||
|
['fr', 'skill.nodejs.description', 'Environnement d\'exécution JavaScript côté serveur'],
|
||||||
|
['en', 'skill.nodejs.description', 'Server-side JavaScript runtime environment'],
|
||||||
|
|
||||||
|
// Skills - Git
|
||||||
|
['fr', 'skill.git.name', 'Git'],
|
||||||
|
['en', 'skill.git.name', 'Git'],
|
||||||
|
['fr', 'skill.git.description', 'Système de contrôle de version distribué'],
|
||||||
|
['en', 'skill.git.description', 'Distributed version control system'],
|
||||||
|
|
||||||
|
// Skills - Docker
|
||||||
|
['fr', 'skill.docker.name', 'Docker'],
|
||||||
|
['en', 'skill.docker.name', 'Docker'],
|
||||||
|
['fr', 'skill.docker.description', 'Plateforme de conteneurisation pour le déploiement d\'applications'],
|
||||||
|
['en', 'skill.docker.description', 'Containerization platform for application deployment'],
|
||||||
|
|
||||||
|
// Skills - Communication
|
||||||
|
['fr', 'skill.communication.name', 'Communication'],
|
||||||
|
['en', 'skill.communication.name', 'Communication'],
|
||||||
|
['fr', 'skill.communication.description', 'Capacité à transmettre des idées clairement et efficacement'],
|
||||||
|
['en', 'skill.communication.description', 'Ability to convey ideas clearly and effectively'],
|
||||||
|
|
||||||
|
// Projects - Skycel Portfolio
|
||||||
|
['fr', 'project.skycel-portfolio.title', 'Skycel Portfolio'],
|
||||||
|
['en', 'project.skycel-portfolio.title', 'Skycel Portfolio'],
|
||||||
|
['fr', 'project.skycel-portfolio.description', 'Portfolio gamifié avec système de progression et arbre de compétences interactif. Construit avec Nuxt 4 et Laravel 12.'],
|
||||||
|
['en', 'project.skycel-portfolio.description', 'Gamified portfolio with progression system and interactive skill tree. Built with Nuxt 4 and Laravel 12.'],
|
||||||
|
['fr', 'project.skycel-portfolio.short_description', 'Portfolio gamifié avec arbre de compétences'],
|
||||||
|
['en', 'project.skycel-portfolio.short_description', 'Gamified portfolio with skill tree'],
|
||||||
|
|
||||||
|
// Projects - E-commerce
|
||||||
|
['fr', 'project.ecommerce-app.title', 'E-Commerce App'],
|
||||||
|
['en', 'project.ecommerce-app.title', 'E-Commerce App'],
|
||||||
|
['fr', 'project.ecommerce-app.description', 'Application e-commerce complète avec panier, paiement Stripe et gestion des commandes. Interface responsive et performante.'],
|
||||||
|
['en', 'project.ecommerce-app.description', 'Full e-commerce application with cart, Stripe payment and order management. Responsive and performant interface.'],
|
||||||
|
['fr', 'project.ecommerce-app.short_description', 'Application e-commerce avec paiement Stripe'],
|
||||||
|
['en', 'project.ecommerce-app.short_description', 'E-commerce app with Stripe payment'],
|
||||||
|
|
||||||
|
// Projects - Dashboard
|
||||||
|
['fr', 'project.analytics-dashboard.title', 'Analytics Dashboard'],
|
||||||
|
['en', 'project.analytics-dashboard.title', 'Analytics Dashboard'],
|
||||||
|
['fr', 'project.analytics-dashboard.description', 'Tableau de bord analytique avec visualisations de données en temps réel, graphiques interactifs et export de rapports.'],
|
||||||
|
['en', 'project.analytics-dashboard.description', 'Analytics dashboard with real-time data visualizations, interactive charts and report export.'],
|
||||||
|
['fr', 'project.analytics-dashboard.short_description', 'Dashboard analytique avec données temps réel'],
|
||||||
|
['en', 'project.analytics-dashboard.short_description', 'Analytics dashboard with real-time data'],
|
||||||
|
|
||||||
|
// Skill-Project level descriptions
|
||||||
|
['fr', 'skill_project.skycel-portfolio.vuejs.level_description', 'Utilisation avancée de Vue 3 avec Composition API et réactivité'],
|
||||||
|
['en', 'skill_project.skycel-portfolio.vuejs.level_description', 'Advanced use of Vue 3 with Composition API and reactivity'],
|
||||||
|
['fr', 'skill_project.skycel-portfolio.nuxt.level_description', 'Premier projet avec Nuxt 4 et SSR'],
|
||||||
|
['en', 'skill_project.skycel-portfolio.nuxt.level_description', 'First project with Nuxt 4 and SSR'],
|
||||||
|
['fr', 'skill_project.skycel-portfolio.laravel.level_description', 'API REST complète avec middleware et Eloquent'],
|
||||||
|
['en', 'skill_project.skycel-portfolio.laravel.level_description', 'Complete REST API with middleware and Eloquent'],
|
||||||
|
['fr', 'skill_project.skycel-portfolio.tailwindcss.level_description', 'Design system complet avec tokens personnalisés'],
|
||||||
|
['en', 'skill_project.skycel-portfolio.tailwindcss.level_description', 'Complete design system with custom tokens'],
|
||||||
|
['fr', 'skill_project.ecommerce-app.laravel.level_description', 'Backend e-commerce avec intégration Stripe'],
|
||||||
|
['en', 'skill_project.ecommerce-app.laravel.level_description', 'E-commerce backend with Stripe integration'],
|
||||||
|
['fr', 'skill_project.ecommerce-app.vuejs.level_description', 'Interface dynamique avec gestion du panier'],
|
||||||
|
['en', 'skill_project.ecommerce-app.vuejs.level_description', 'Dynamic interface with cart management'],
|
||||||
|
['fr', 'skill_project.analytics-dashboard.typescript.level_description', 'Typage strict pour la fiabilité des données'],
|
||||||
|
['en', 'skill_project.analytics-dashboard.typescript.level_description', 'Strict typing for data reliability'],
|
||||||
|
['fr', 'skill_project.analytics-dashboard.nodejs.level_description', 'API backend avec agrégation de données'],
|
||||||
|
['en', 'skill_project.analytics-dashboard.nodejs.level_description', 'Backend API with data aggregation'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($translations as [$lang, $key_name, $value]) {
|
||||||
|
Translation::create([
|
||||||
|
'lang' => $lang,
|
||||||
|
'key_name' => $key_name,
|
||||||
|
'value' => $value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Story 1.2: Base de données et migrations initiales
|
# Story 1.2: Base de données et migrations initiales
|
||||||
|
|
||||||
Status: ready-for-dev
|
Status: review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
@@ -20,71 +20,71 @@ so that l'API peut servir du contenu bilingue.
|
|||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [ ] **Task 1: Configuration connexion MariaDB** (AC: #1)
|
- [x] **Task 1: Configuration connexion MariaDB** (AC: #1)
|
||||||
- [ ] Vérifier que MariaDB est installé et accessible
|
- [x] Vérifier que MariaDB est installé et accessible
|
||||||
- [ ] Créer la base de données `skycel` si elle n'existe pas
|
- [x] Créer la base de données `skycel` si elle n'existe pas
|
||||||
- [ ] Configurer `api/.env` avec les variables DB_* correctes
|
- [x] Configurer `api/.env` avec les variables DB_* correctes
|
||||||
- [ ] Tester la connexion avec `php artisan db:show`
|
- [x] Tester la connexion avec `php artisan db:show`
|
||||||
|
|
||||||
- [ ] **Task 2: Migration table translations** (AC: #1)
|
- [x] **Task 2: Migration table translations** (AC: #1)
|
||||||
- [ ] Créer migration `create_translations_table`
|
- [x] Créer migration `create_translations_table`
|
||||||
- [ ] Colonnes: id, lang (VARCHAR 5), key_name (VARCHAR 255), value (TEXT), timestamps
|
- [x] Colonnes: id, lang (VARCHAR 5), key_name (VARCHAR 255), value (TEXT), timestamps
|
||||||
- [ ] Index unique composite sur (lang, key_name)
|
- [x] Index unique composite sur (lang, key_name)
|
||||||
- [ ] Index simple sur lang pour les requêtes par langue
|
- [x] Index simple sur lang pour les requêtes par langue
|
||||||
|
|
||||||
- [ ] **Task 3: Migration table projects** (AC: #2)
|
- [x] **Task 3: Migration table projects** (AC: #2)
|
||||||
- [ ] Créer migration `create_projects_table`
|
- [x] Créer migration `create_projects_table`
|
||||||
- [ ] Colonnes: id, slug (unique), title_key, description_key, short_description_key, image, url (nullable), github_url (nullable), date_completed (date), is_featured (boolean, default false), display_order (integer, default 0), timestamps
|
- [x] Colonnes: id, slug (unique), title_key, description_key, short_description_key, image, url (nullable), github_url (nullable), date_completed (date), is_featured (boolean, default false), display_order (integer, default 0), timestamps
|
||||||
- [ ] Index sur slug (unique)
|
- [x] Index sur slug (unique)
|
||||||
- [ ] Index sur display_order pour le tri
|
- [x] Index sur display_order pour le tri
|
||||||
|
|
||||||
- [ ] **Task 4: Migration table skills** (AC: #3)
|
- [x] **Task 4: Migration table skills** (AC: #3)
|
||||||
- [ ] Créer migration `create_skills_table`
|
- [x] Créer migration `create_skills_table`
|
||||||
- [ ] Colonnes: id, slug (unique), name_key, description_key, icon (nullable), category (enum ou string: Frontend, Backend, Tools, Soft skills), max_level (integer, default 5), display_order (integer, default 0), timestamps
|
- [x] Colonnes: id, slug (unique), name_key, description_key, icon (nullable), category (enum ou string: Frontend, Backend, Tools, Soft skills), max_level (integer, default 5), display_order (integer, default 0), timestamps
|
||||||
- [ ] Index sur slug (unique)
|
- [x] Index sur slug (unique)
|
||||||
- [ ] Index sur category pour le filtrage
|
- [x] Index sur category pour le filtrage
|
||||||
|
|
||||||
- [ ] **Task 5: Migration table pivot skill_project** (AC: #4)
|
- [x] **Task 5: Migration table pivot skill_project** (AC: #4)
|
||||||
- [ ] Créer migration `create_skill_project_table`
|
- [x] Créer migration `create_skill_project_table`
|
||||||
- [ ] Colonnes: id, skill_id (FK), project_id (FK), level_before (integer), level_after (integer), level_description_key (nullable), timestamps
|
- [x] Colonnes: id, skill_id (FK), project_id (FK), level_before (integer), level_after (integer), level_description_key (nullable), timestamps
|
||||||
- [ ] Foreign key skill_id → skills.id avec ON DELETE CASCADE
|
- [x] Foreign key skill_id → skills.id avec ON DELETE CASCADE
|
||||||
- [ ] Foreign key project_id → projects.id avec ON DELETE CASCADE
|
- [x] Foreign key project_id → projects.id avec ON DELETE CASCADE
|
||||||
- [ ] Index composite sur (skill_id, project_id) pour éviter les doublons
|
- [x] Index composite sur (skill_id, project_id) pour éviter les doublons
|
||||||
|
|
||||||
- [ ] **Task 6: Model Translation** (AC: #5)
|
- [x] **Task 6: Model Translation** (AC: #5)
|
||||||
- [ ] Créer `app/Models/Translation.php`
|
- [x] Créer `app/Models/Translation.php`
|
||||||
- [ ] Propriétés fillable: lang, key_name, value
|
- [x] Propriétés fillable: lang, key_name, value
|
||||||
- [ ] Scope `scopeForLang($query, $lang)` pour filtrer par langue
|
- [x] Scope `scopeForLang($query, $lang)` pour filtrer par langue
|
||||||
- [ ] Méthode statique `getTranslation($key, $lang, $fallback = 'fr')`
|
- [x] Méthode statique `getTranslation($key, $lang, $fallback = 'fr')`
|
||||||
|
|
||||||
- [ ] **Task 7: Model Project avec relations** (AC: #5)
|
- [x] **Task 7: Model Project avec relations** (AC: #5)
|
||||||
- [ ] Créer `app/Models/Project.php`
|
- [x] Créer `app/Models/Project.php`
|
||||||
- [ ] Propriétés fillable: slug, title_key, description_key, short_description_key, image, url, github_url, date_completed, is_featured, display_order
|
- [x] Propriétés fillable: slug, title_key, description_key, short_description_key, image, url, github_url, date_completed, is_featured, display_order
|
||||||
- [ ] Casts: date_completed → date, is_featured → boolean
|
- [x] Casts: date_completed → date, is_featured → boolean
|
||||||
- [ ] Relation `skills()`: belongsToMany(Skill::class)->withPivot(['level_before', 'level_after', 'level_description_key'])->withTimestamps()
|
- [x] Relation `skills()`: belongsToMany(Skill::class)->withPivot(['level_before', 'level_after', 'level_description_key'])->withTimestamps()
|
||||||
- [ ] Scope `scopeFeatured($query)` pour les projets mis en avant
|
- [x] Scope `scopeFeatured($query)` pour les projets mis en avant
|
||||||
- [ ] Scope `scopeOrdered($query)` pour le tri par display_order
|
- [x] Scope `scopeOrdered($query)` pour le tri par display_order
|
||||||
|
|
||||||
- [ ] **Task 8: Model Skill avec relations** (AC: #5)
|
- [x] **Task 8: Model Skill avec relations** (AC: #5)
|
||||||
- [ ] Créer `app/Models/Skill.php`
|
- [x] Créer `app/Models/Skill.php`
|
||||||
- [ ] Propriétés fillable: slug, name_key, description_key, icon, category, max_level, display_order
|
- [x] Propriétés fillable: slug, name_key, description_key, icon, category, max_level, display_order
|
||||||
- [ ] Relation `projects()`: belongsToMany(Project::class)->withPivot(['level_before', 'level_after', 'level_description_key'])->withTimestamps()
|
- [x] Relation `projects()`: belongsToMany(Project::class)->withPivot(['level_before', 'level_after', 'level_description_key'])->withTimestamps()
|
||||||
- [ ] Scope `scopeByCategory($query, $category)` pour filtrer par catégorie
|
- [x] Scope `scopeByCategory($query, $category)` pour filtrer par catégorie
|
||||||
- [ ] Scope `scopeOrdered($query)` pour le tri par display_order
|
- [x] Scope `scopeOrdered($query)` pour le tri par display_order
|
||||||
|
|
||||||
- [ ] **Task 9: Seeders de base** (AC: #6, #7)
|
- [x] **Task 9: Seeders de base** (AC: #6, #7)
|
||||||
- [ ] Créer `database/seeders/TranslationSeeder.php` avec traductions FR et EN de test
|
- [x] Créer `database/seeders/TranslationSeeder.php` avec traductions FR et EN de test
|
||||||
- [ ] Créer `database/seeders/SkillSeeder.php` avec 8-10 compétences de test (Frontend, Backend, Tools)
|
- [x] Créer `database/seeders/SkillSeeder.php` avec 8-10 compétences de test (Frontend, Backend, Tools)
|
||||||
- [ ] Créer `database/seeders/ProjectSeeder.php` avec 3-4 projets de test
|
- [x] Créer `database/seeders/ProjectSeeder.php` avec 3-4 projets de test
|
||||||
- [ ] Créer `database/seeders/SkillProjectSeeder.php` pour lier compétences et projets
|
- [x] Créer `database/seeders/SkillProjectSeeder.php` pour lier compétences et projets
|
||||||
- [ ] Mettre à jour `DatabaseSeeder.php` pour appeler les seeders dans l'ordre correct (translations → skills → projects → skill_project)
|
- [x] Mettre à jour `DatabaseSeeder.php` pour appeler les seeders dans l'ordre correct (translations → skills → projects → skill_project)
|
||||||
|
|
||||||
- [ ] **Task 10: Validation finale** (AC: tous)
|
- [x] **Task 10: Validation finale** (AC: tous)
|
||||||
- [ ] `php artisan migrate:fresh` fonctionne sans erreur
|
- [x] `php artisan migrate:fresh` fonctionne sans erreur
|
||||||
- [ ] `php artisan db:seed` fonctionne sans erreur
|
- [x] `php artisan db:seed` fonctionne sans erreur
|
||||||
- [ ] Vérifier en BDD que les tables sont créées avec les bons schémas
|
- [x] Vérifier en BDD que les tables sont créées avec les bons schémas
|
||||||
- [ ] Vérifier que les relations fonctionnent: `Project::first()->skills` et `Skill::first()->projects`
|
- [x] Vérifier que les relations fonctionnent: `Project::first()->skills` et `Skill::first()->projects`
|
||||||
- [ ] Vérifier que les traductions fonctionnent: `Translation::getTranslation('project.skycel.title', 'fr')`
|
- [x] Vérifier que les traductions fonctionnent: `Translation::getTranslation('project.skycel-portfolio.title', 'fr')`
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
@@ -259,16 +259,41 @@ api/
|
|||||||
|
|
||||||
### Agent Model Used
|
### Agent Model Used
|
||||||
|
|
||||||
{{agent_model_name_version}}
|
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||||
|
|
||||||
### Debug Log References
|
### Debug Log References
|
||||||
|
|
||||||
|
- Pivot table naming: Laravel attend `project_skill` (ordre alphabétique), mais la story spécifie `skill_project`. Résolu en ajoutant le nom de table explicite dans les relations `belongsToMany`.
|
||||||
|
- Connexion MySQL: `DB_PASSWORD=` (vide) requis pour root@localhost sur Laragon.
|
||||||
|
|
||||||
### Completion Notes List
|
### Completion Notes List
|
||||||
|
|
||||||
|
- 4 migrations créées et fonctionnelles (translations, projects, skills, skill_project)
|
||||||
|
- 3 models Eloquent avec relations belongsToMany, scopes et casts
|
||||||
|
- 4 seeders de test: 37 traductions FR + 37 EN, 10 skills, 3 projets, 12 liens skill_project
|
||||||
|
- `migrate:fresh --seed` fonctionne sans erreur
|
||||||
|
- Relations vérifiées: Project->skills (5), Skill->projects (2)
|
||||||
|
- Traductions vérifiées: FR, EN et fallback fonctionnent
|
||||||
|
- Scopes vérifiés: featured (2), byCategory Frontend (4), Backend (3)
|
||||||
|
|
||||||
### Change Log
|
### Change Log
|
||||||
| Date | Change | Author |
|
| Date | Change | Author |
|
||||||
|------|--------|--------|
|
|------|--------|--------|
|
||||||
| 2026-02-03 | Story créée avec contexte complet | SM Agent |
|
| 2026-02-03 | Story créée avec contexte complet | SM Agent |
|
||||||
|
| 2026-02-05 | Tasks 1-10 implémentées et validées | Dev Agent (Claude Opus 4.5) |
|
||||||
|
|
||||||
### File List
|
### File List
|
||||||
|
|
||||||
|
- `api/database/migrations/2026_02_05_000001_create_translations_table.php` — CRÉÉ
|
||||||
|
- `api/database/migrations/2026_02_05_000002_create_projects_table.php` — CRÉÉ
|
||||||
|
- `api/database/migrations/2026_02_05_000003_create_skills_table.php` — CRÉÉ
|
||||||
|
- `api/database/migrations/2026_02_05_000004_create_skill_project_table.php` — CRÉÉ
|
||||||
|
- `api/app/Models/Translation.php` — CRÉÉ
|
||||||
|
- `api/app/Models/Project.php` — CRÉÉ
|
||||||
|
- `api/app/Models/Skill.php` — CRÉÉ
|
||||||
|
- `api/database/seeders/TranslationSeeder.php` — CRÉÉ
|
||||||
|
- `api/database/seeders/SkillSeeder.php` — CRÉÉ
|
||||||
|
- `api/database/seeders/ProjectSeeder.php` — CRÉÉ
|
||||||
|
- `api/database/seeders/SkillProjectSeeder.php` — CRÉÉ
|
||||||
|
- `api/database/seeders/DatabaseSeeder.php` — MODIFIÉ
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ development_status:
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
epic-1: in-progress
|
epic-1: in-progress
|
||||||
1-1-initialisation-monorepo-infrastructure: review
|
1-1-initialisation-monorepo-infrastructure: review
|
||||||
1-2-base-donnees-migrations-initiales: ready-for-dev
|
1-2-base-donnees-migrations-initiales: review
|
||||||
1-3-systeme-i18n-frontend-api-bilingue: ready-for-dev
|
1-3-systeme-i18n-frontend-api-bilingue: ready-for-dev
|
||||||
1-4-layouts-routing-transitions-page: ready-for-dev
|
1-4-layouts-routing-transitions-page: ready-for-dev
|
||||||
1-5-landing-page-choix-heros: ready-for-dev
|
1-5-landing-page-choix-heros: ready-for-dev
|
||||||
|
|||||||
Reference in New Issue
Block a user