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>
17 KiB
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
- Given les migrations Laravel sont exécutées When
php artisan migrateest lancé Then la tableeaster_eggsest créée (id, slug, location, trigger_type ENUM, reward_type ENUM, reward_key, difficulty, is_active, timestamps) - And les trigger_types incluent : click, hover, konami, scroll, sequence
- And les reward_types incluent : snippet, anecdote, image, badge
- And les seeders insèrent 5-10 easter eggs avec leurs récompenses traduites
- Given l'API
/api/easter-eggsest appelée When la requête est faite Then les métadonnées des easter eggs actifs sont retournées (slug, location, trigger_type) - And les réponses/récompenses ne sont PAS incluses (pour éviter la triche)
- Given l'API
/api/easter-eggs/{slug}/validateest appelée When un slug valide est fourni Then la récompense traduite est retournée - 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
- Créer migration
-
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
- Créer
-
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
- Créer
-
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
- Créer
-
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
- Méthode
-
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)
- Ajouter
-
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é
- Créer
-
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 |