🎉 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:
2026-02-05 02:08:56 +01:00
commit ec1ae92799
116 changed files with 55669 additions and 0 deletions

View File

@@ -0,0 +1,532 @@
# Story 4.4: Table easter_eggs et système de détection
Status: ready-for-dev
## Story
As a développeur,
I want une infrastructure pour gérer les easter eggs cachés,
so that je peux ajouter des surprises récompensant l'exploration.
## Acceptance Criteria
1. **Given** les migrations Laravel sont exécutées **When** `php artisan migrate` est lancé **Then** la table `easter_eggs` est créée (id, slug, location, trigger_type ENUM, reward_type ENUM, reward_key, difficulty, is_active, timestamps)
2. **And** les trigger_types incluent : click, hover, konami, scroll, sequence
3. **And** les reward_types incluent : snippet, anecdote, image, badge
4. **And** les seeders insèrent 5-10 easter eggs avec leurs récompenses traduites
5. **Given** l'API `/api/easter-eggs` est appelée **When** la requête est faite **Then** les métadonnées des easter eggs actifs sont retournées (slug, location, trigger_type)
6. **And** les réponses/récompenses ne sont PAS incluses (pour éviter la triche)
7. **Given** l'API `/api/easter-eggs/{slug}/validate` est appelée **When** un slug valide est fourni **Then** la récompense traduite est retournée
8. **And** l'easter egg est marqué comme trouvé côté client (store)
## Tasks / Subtasks
- [ ] **Task 1: Créer la migration table easter_eggs** (AC: #1, #2, #3)
- [ ] Créer migration `create_easter_eggs_table`
- [ ] Colonnes : id, slug (unique), location, trigger_type (ENUM), reward_type (ENUM), reward_key, difficulty (1-5), is_active (boolean), timestamps
- [ ] ENUMs pour trigger_type et reward_type
- [ ] **Task 2: Créer le Model EasterEgg** (AC: #1)
- [ ] Créer `app/Models/EasterEgg.php`
- [ ] Définir les fillable et casts
- [ ] Scope `active()` pour les easter eggs actifs
- [ ] Relation avec translations pour reward_key
- [ ] **Task 3: Créer le Seeder des easter eggs** (AC: #4)
- [ ] Créer `database/seeders/EasterEggSeeder.php`
- [ ] 5-10 easter eggs avec variété de triggers et récompenses
- [ ] Ajouter les traductions FR et EN pour les récompenses
- [ ] **Task 4: Créer l'endpoint liste des easter eggs** (AC: #5, #6)
- [ ] Créer `app/Http/Controllers/Api/EasterEggController.php`
- [ ] Méthode `index()` retournant slug, location, trigger_type
- [ ] NE PAS inclure reward_key ou détails de la récompense
- [ ] **Task 5: Créer l'endpoint validation** (AC: #7)
- [ ] Méthode `validate($slug)` retournant la récompense
- [ ] Traduire selon Accept-Language
- [ ] Retourner 404 si slug invalide
- [ ] **Task 6: Créer le store côté client** (AC: #8)
- [ ] Ajouter `easterEggsFound: string[]` dans useProgressionStore
- [ ] Méthode `markEasterEggFound(slug)`
- [ ] Getter `easterEggsCount` (trouvés/total)
- [ ] **Task 7: Créer le composable useFetchEasterEggs**
- [ ] Créer `frontend/app/composables/useFetchEasterEggs.ts`
- [ ] Méthode `fetchList()` pour récupérer les métadonnées
- [ ] Méthode `validate(slug)` pour valider un easter egg trouvé
- [ ] **Task 8: Tests et validation**
- [ ] Exécuter les migrations
- [ ] Vérifier le seeding
- [ ] Tester l'API liste (sans récompenses)
- [ ] Tester l'API validation (avec récompenses)
## Dev Notes
### Migration easter_eggs
```php
<?php
// database/migrations/2026_02_04_000003_create_easter_eggs_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('easter_eggs', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('location'); // Page ou zone où se trouve l'easter egg
$table->enum('trigger_type', ['click', 'hover', 'konami', 'scroll', 'sequence']);
$table->enum('reward_type', ['snippet', 'anecdote', 'image', 'badge']);
$table->string('reward_key'); // Clé de traduction pour la récompense
$table->unsignedTinyInteger('difficulty')->default(1); // 1-5
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('is_active');
$table->index('location');
});
}
public function down(): void
{
Schema::dropIfExists('easter_eggs');
}
};
```
### Model EasterEgg
```php
<?php
// api/app/Models/EasterEgg.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EasterEgg extends Model
{
protected $fillable = [
'slug',
'location',
'trigger_type',
'reward_type',
'reward_key',
'difficulty',
'is_active',
];
protected $casts = [
'difficulty' => 'integer',
'is_active' => 'boolean',
];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeByLocation($query, string $location)
{
return $query->where('location', $location);
}
public function getReward(string $lang = 'fr'): ?string
{
return Translation::getTranslation($this->reward_key, $lang);
}
}
```
### Seeder des easter eggs
```php
<?php
// database/seeders/EasterEggSeeder.php
namespace Database\Seeders;
use App\Models\EasterEgg;
use App\Models\Translation;
use Illuminate\Database\Seeder;
class EasterEggSeeder extends Seeder
{
public function run(): void
{
$easterEggs = [
// 1. Konami code sur la landing
[
'slug' => 'konami-master',
'location' => 'landing',
'trigger_type' => 'konami',
'reward_type' => 'badge',
'reward_key' => 'easter.konami.reward',
'difficulty' => 3,
],
// 2. Clic sur l'araignée cachée (header)
[
'slug' => 'hidden-spider',
'location' => 'header',
'trigger_type' => 'click',
'reward_type' => 'anecdote',
'reward_key' => 'easter.spider.reward',
'difficulty' => 2,
],
// 3. Hover sur un caractère spécial dans le code (page projets)
[
'slug' => 'secret-comment',
'location' => 'projects',
'trigger_type' => 'hover',
'reward_type' => 'snippet',
'reward_key' => 'easter.comment.reward',
'difficulty' => 2,
],
// 4. Scroll jusqu'en bas de la page parcours
[
'slug' => 'journey-end',
'location' => 'journey',
'trigger_type' => 'scroll',
'reward_type' => 'anecdote',
'reward_key' => 'easter.journey_end.reward',
'difficulty' => 1,
],
// 5. Séquence de clics sur les compétences (Vue, Laravel, TypeScript)
[
'slug' => 'tech-sequence',
'location' => 'skills',
'trigger_type' => 'sequence',
'reward_type' => 'snippet',
'reward_key' => 'easter.tech_seq.reward',
'difficulty' => 4,
],
// 6. Clic sur le logo Skycel 5 fois
[
'slug' => 'logo-clicks',
'location' => 'global',
'trigger_type' => 'click',
'reward_type' => 'image',
'reward_key' => 'easter.logo.reward',
'difficulty' => 2,
],
// 7. Hover sur la date "2022" dans le parcours
[
'slug' => 'founding-date',
'location' => 'journey',
'trigger_type' => 'hover',
'reward_type' => 'anecdote',
'reward_key' => 'easter.founding.reward',
'difficulty' => 2,
],
// 8. Console.log dans les devtools
[
'slug' => 'dev-console',
'location' => 'global',
'trigger_type' => 'sequence',
'reward_type' => 'badge',
'reward_key' => 'easter.console.reward',
'difficulty' => 3,
],
];
foreach ($easterEggs as $egg) {
EasterEgg::create($egg);
}
// Traductions des récompenses
$translations = [
// Konami
['key' => 'easter.konami.reward', 'fr' => "🎮 Badge 'Gamer' débloqué ! Tu connais les classiques.", 'en' => "🎮 'Gamer' badge unlocked! You know the classics."],
// Spider
['key' => 'easter.spider.reward', 'fr' => "🕷️ Tu m'as trouvé ! Je me cache partout sur ce site... Le Bug te surveille toujours.", 'en' => "🕷️ You found me! I hide everywhere on this site... The Bug is always watching."],
// Comment
['key' => 'easter.comment.reward', 'fr' => "// Premier code écrit : console.log('Hello World'); // Tout a commencé là...", 'en' => "// First code written: console.log('Hello World'); // It all started there..."],
// Journey end
['key' => 'easter.journey_end.reward', 'fr' => "Tu as lu jusqu'au bout ? Respect. Le prochain chapitre s'écrit peut-être avec toi.", 'en' => "You read all the way? Respect. The next chapter might be written with you."],
// Tech sequence
['key' => 'easter.tech_seq.reward', 'fr' => "const stack = ['Vue', 'Laravel', 'TypeScript'];\n// La sainte trinité du dev moderne ⚡", 'en' => "const stack = ['Vue', 'Laravel', 'TypeScript'];\n// The holy trinity of modern dev ⚡"],
// Logo
['key' => 'easter.logo.reward', 'fr' => "🖼️ Image secrète débloquée : La première version du logo Skycel (spoiler: c'était moche)", 'en' => "🖼️ Secret image unlocked: The first version of the Skycel logo (spoiler: it was ugly)"],
// Founding
['key' => 'easter.founding.reward', 'fr' => "2022 : l'année où Le Bug est né. Littéralement un bug dans le code qui m'a donné l'idée de la mascotte.", 'en' => "2022: the year The Bug was born. Literally a bug in the code that gave me the mascot idea."],
// Console
['key' => 'easter.console.reward', 'fr' => "🔧 Badge 'Développeur' débloqué ! Tu as vérifié la console comme un vrai dev.", 'en' => "🔧 'Developer' badge unlocked! You checked the console like a real dev."],
];
foreach ($translations as $t) {
Translation::firstOrCreate(
['lang' => 'fr', 'key_name' => $t['key']],
['value' => $t['fr']]
);
Translation::firstOrCreate(
['lang' => 'en', 'key_name' => $t['key']],
['value' => $t['en']]
);
}
}
}
```
### Controller API
```php
<?php
// api/app/Http/Controllers/Api/EasterEggController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EasterEgg;
use Illuminate\Http\Request;
class EasterEggController extends Controller
{
/**
* Liste les easter eggs actifs (sans révéler les récompenses)
*/
public function index()
{
$easterEggs = EasterEgg::active()
->select('slug', 'location', 'trigger_type', 'difficulty')
->get();
return response()->json([
'data' => $easterEggs,
'meta' => [
'total' => $easterEggs->count(),
],
]);
}
/**
* Valide un easter egg et retourne la récompense
*/
public function validate(Request $request, string $slug)
{
$easterEgg = EasterEgg::active()->where('slug', $slug)->first();
if (!$easterEgg) {
return response()->json([
'error' => [
'code' => 'EASTER_EGG_NOT_FOUND',
'message' => 'Easter egg not found or inactive',
],
], 404);
}
$lang = $request->header('Accept-Language', 'fr');
$reward = $easterEgg->getReward($lang);
return response()->json([
'data' => [
'slug' => $easterEgg->slug,
'reward_type' => $easterEgg->reward_type,
'reward' => $reward,
'difficulty' => $easterEgg->difficulty,
],
]);
}
}
```
### Routes API
```php
// api/routes/api.php
Route::get('/easter-eggs', [EasterEggController::class, 'index']);
Route::post('/easter-eggs/{slug}/validate', [EasterEggController::class, 'validate']);
```
### Extension du store progression
```typescript
// À ajouter dans frontend/app/stores/progression.ts
// État
const easterEggsFound = ref<string[]>([])
// Actions
function markEasterEggFound(slug: string) {
if (!easterEggsFound.value.includes(slug)) {
easterEggsFound.value.push(slug)
}
}
// Getters
const easterEggsFoundCount = computed(() => easterEggsFound.value.length)
// Export
return {
// ... existing ...
easterEggsFound,
easterEggsFoundCount,
markEasterEggFound,
}
```
### Composable useFetchEasterEggs
```typescript
// frontend/app/composables/useFetchEasterEggs.ts
interface EasterEggMeta {
slug: string
location: string
trigger_type: 'click' | 'hover' | 'konami' | 'scroll' | 'sequence'
difficulty: number
}
interface EasterEggReward {
slug: string
reward_type: 'snippet' | 'anecdote' | 'image' | 'badge'
reward: string
difficulty: number
}
export function useFetchEasterEggs() {
const config = useRuntimeConfig()
const { locale } = useI18n()
// Cache des easter eggs disponibles
const availableEasterEggs = ref<EasterEggMeta[]>([])
const isLoaded = ref(false)
async function fetchList(): Promise<EasterEggMeta[]> {
if (isLoaded.value) return availableEasterEggs.value
const response = await $fetch<{ data: EasterEggMeta[] }>('/easter-eggs', {
baseURL: config.public.apiUrl,
headers: {
'X-API-Key': config.public.apiKey,
},
})
availableEasterEggs.value = response.data
isLoaded.value = true
return response.data
}
async function validate(slug: string): Promise<EasterEggReward | null> {
try {
const response = await $fetch<{ data: EasterEggReward }>(`/easter-eggs/${slug}/validate`, {
method: 'POST',
baseURL: config.public.apiUrl,
headers: {
'X-API-Key': config.public.apiKey,
'Accept-Language': locale.value,
},
})
return response.data
} catch (error) {
console.error('Failed to validate easter egg:', error)
return null
}
}
function getByLocation(location: string): EasterEggMeta[] {
return availableEasterEggs.value.filter(e => e.location === location || e.location === 'global')
}
return {
availableEasterEggs,
fetchList,
validate,
getByLocation,
}
}
```
### Tableau des easter eggs
| Slug | Location | Trigger | Type | Difficulté |
|------|----------|---------|------|------------|
| konami-master | landing | konami | badge | 3/5 |
| hidden-spider | header | click | anecdote | 2/5 |
| secret-comment | projects | hover | snippet | 2/5 |
| journey-end | journey | scroll | anecdote | 1/5 |
| tech-sequence | skills | sequence | snippet | 4/5 |
| logo-clicks | global | click | image | 2/5 |
| founding-date | journey | hover | anecdote | 2/5 |
| dev-console | global | sequence | badge | 3/5 |
### Dépendances
**Cette story nécessite :**
- Story 1.2 : Table translations
- Story 3.5 : Store de progression
**Cette story prépare pour :**
- Story 4.5 : Implémentation UI des easter eggs
### Project Structure Notes
**Fichiers à créer :**
```
api/
├── app/Models/
│ └── EasterEgg.php # CRÉER
├── app/Http/Controllers/Api/
│ └── EasterEggController.php # CRÉER
└── database/
├── migrations/
│ └── 2026_02_04_000003_create_easter_eggs_table.php # CRÉER
└── seeders/
└── EasterEggSeeder.php # CRÉER
frontend/app/composables/
└── useFetchEasterEggs.ts # CRÉER
```
**Fichiers à modifier :**
```
api/routes/api.php # AJOUTER routes easter-eggs
api/database/seeders/DatabaseSeeder.php # APPELER EasterEggSeeder
frontend/app/stores/progression.ts # AJOUTER easterEggsFound
```
### References
- [Source: docs/planning-artifacts/epics.md#Story-4.4]
- [Source: docs/planning-artifacts/ux-design-specification.md#Easter-Eggs]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Easter-Eggs]
### Technical Requirements
| Requirement | Value | Source |
|-------------|-------|--------|
| Nombre d'easter eggs | 5-10 | Epics |
| Trigger types | click, hover, konami, scroll, sequence | Epics |
| Reward types | snippet, anecdote, image, badge | Epics |
| API sans spoil | Liste sans récompenses | Epics |
## 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