# 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 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 '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 '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 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([]) // 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([]) const isLoaded = ref(false) async function fetchList(): Promise { 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 { 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