Files
Portfolio-Game/docs/implementation-artifacts/4-4-table-easter-eggs-systeme-detection.md
skycel ec1ae92799 🎉 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>
2026-02-05 02:08:56 +01:00

17 KiB

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
// 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
// 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
// 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
// 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

// api/routes/api.php
Route::get('/easter-eggs', [EasterEggController::class, 'index']);
Route::post('/easter-eggs/{slug}/validate', [EasterEggController::class, 'validate']);

Extension du store progression

// À 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

// 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