feat(epic-4): chemins narratifs, easter eggs, challenge et contact

Epic 4: Chemins Narratifs, Challenge & Contact

Stories implementees:
- 4.1: Composant ChoiceCards pour choix narratifs binaires
- 4.2: Sequence d'intro narrative avec Le Bug
- 4.3: Chemins narratifs differencies avec useNarrativePath
- 4.4: Table easter_eggs et systeme de detection (API + composable)
- 4.5: Easter eggs UI (popup, notification, collection)
- 4.6: Page challenge avec puzzle de code
- 4.7: Page revelation "Monde de Code"
- 4.8: Page contact avec formulaire et stats

Fichiers crees:
- Frontend: ChoiceCards, IntroSequence, ZoneEndChoice, EasterEggPopup,
  CodePuzzle, ChallengeSuccess, CodeWorld, et pages intro/challenge/revelation
- API: EasterEggController, Model, Migration, Seeder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 13:35:12 +01:00
parent 64b1a33d10
commit 7e87a341a2
38 changed files with 3037 additions and 96 deletions

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EasterEgg;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EasterEggController extends Controller
{
/**
* Liste les easter eggs actifs (sans révéler les récompenses)
*/
public function index(): JsonResponse
{
$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): JsonResponse
{
$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 = app()->getLocale();
$reward = $easterEgg->getReward($lang);
return response()->json([
'data' => [
'slug' => $easterEgg->slug,
'reward_type' => $easterEgg->reward_type,
'reward' => $reward,
'difficulty' => $easterEgg->difficulty,
],
]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
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(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function scopeByLocation(Builder $query, string $location): Builder
{
return $query->where('location', $location);
}
public function getReward(string $lang = 'fr'): ?string
{
return Translation::getTranslation($this->reward_key, $lang);
}
}

View File

@@ -0,0 +1,31 @@
<?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');
}
};

View File

@@ -18,6 +18,7 @@ class DatabaseSeeder extends Seeder
SkillProjectSeeder::class,
TestimonialSeeder::class,
NarratorTextSeeder::class,
EasterEggSeeder::class,
]);
}
}

View File

@@ -0,0 +1,155 @@
<?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::firstOrCreate(['slug' => $egg['slug']], $egg);
}
// Traductions des récompenses
$translations = [
// Konami
[
'key' => 'easter.konami.reward',
'fr' => "Badge 'Gamer' debloque ! Tu connais les classiques.",
'en' => "'Gamer' badge unlocked! You know the classics.",
],
// Spider
[
'key' => 'easter.spider.reward',
'fr' => "Tu m'as trouve ! 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 ecrit : console.log('Hello World'); // Tout a commence la...",
'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'ecrit peut-etre 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 trinite du dev moderne",
'en' => "const stack = ['Vue', 'Laravel', 'TypeScript'];\n// The holy trinity of modern dev",
],
// Logo
[
'key' => 'easter.logo.reward',
'fr' => "Image secrete debloquee : La premiere version du logo Skycel (spoiler: c'etait moche)",
'en' => "Secret image unlocked: The first version of the Skycel logo (spoiler: it was ugly)",
],
// Founding
[
'key' => 'easter.founding.reward',
'fr' => "2022 : l'annee ou Le Bug est ne. Litteralement un bug dans le code qui m'a donne l'idee 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 'Developpeur' debloque ! Tu as verifie 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']]
);
}
}
}

View File

@@ -23,6 +23,18 @@ class NarratorTextSeeder extends Seeder
['context' => 'intro', 'text_key' => 'narrator.intro.dev.1', 'variant' => 1, 'hero_type' => 'dev'],
['context' => 'intro', 'text_key' => 'narrator.intro.dev.2', 'variant' => 2, 'hero_type' => 'dev'],
// INTRO SEQUENCE 1 - Recruteur
['context' => 'intro_sequence_1', 'text_key' => 'narrator.intro_seq.1.recruteur', 'variant' => 1, 'hero_type' => 'recruteur'],
// INTRO SEQUENCE 1 - Client/Dev
['context' => 'intro_sequence_1', 'text_key' => 'narrator.intro_seq.1.casual', 'variant' => 1, 'hero_type' => 'client'],
['context' => 'intro_sequence_1', 'text_key' => 'narrator.intro_seq.1.casual', 'variant' => 1, 'hero_type' => 'dev'],
// INTRO SEQUENCE 2 - Tous
['context' => 'intro_sequence_2', 'text_key' => 'narrator.intro_seq.2', 'variant' => 1, 'hero_type' => null],
// INTRO SEQUENCE 3 - Tous
['context' => 'intro_sequence_3', 'text_key' => 'narrator.intro_seq.3', 'variant' => 1, 'hero_type' => null],
// TRANSITIONS
['context' => 'transition_projects', 'text_key' => 'narrator.transition.projects.1', 'variant' => 1, 'hero_type' => null],
['context' => 'transition_projects', 'text_key' => 'narrator.transition.projects.2', 'variant' => 2, 'hero_type' => null],
@@ -214,6 +226,31 @@ class NarratorTextSeeder extends Seeder
'fr' => "Tiens, un visage familier ! Content de te revoir, voyageur.",
'en' => "Well, a familiar face! Good to see you again, traveler.",
],
// Intro Sequence 1 - Recruteur
[
'key' => 'narrator.intro_seq.1.recruteur',
'fr' => "Bienvenue dans mon domaine, voyageur... Je suis Le Bug, et je vais vous guider dans cette aventure.",
'en' => "Welcome to my domain, traveler... I am The Bug, and I will guide you through this adventure.",
],
// Intro Sequence 1 - Casual
[
'key' => 'narrator.intro_seq.1.casual',
'fr' => "Hey ! Bienvenue chez moi. Je suis Le Bug, ton guide pour cette aventure.",
'en' => "Hey! Welcome to my place. I'm The Bug, your guide for this adventure.",
],
// Intro Sequence 2
[
'key' => 'narrator.intro_seq.2',
'fr' => "Il y a quelqu'un ici que tu cherches... Un développeur mystérieux qui a créé tout ce que tu vois autour de toi.",
'en' => "There's someone here you're looking for... A mysterious developer who created everything you see around you.",
],
// Intro Sequence 3
[
'key' => 'narrator.intro_seq.3',
'fr' => "Pour le trouver, tu devras explorer ce monde. Chaque zone cache une partie de son histoire. Es-tu prêt ?",
'en' => "To find them, you'll have to explore this world. Each zone hides a part of their story. Are you ready?",
],
];
foreach ($translations as $t) {

View File

@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\EasterEggController;
use App\Http\Controllers\Api\NarratorController;
use App\Http\Controllers\Api\ProjectController;
use App\Http\Controllers\Api\SkillController;
@@ -16,3 +17,5 @@ Route::get('/skills', [SkillController::class, 'index']);
Route::get('/skills/{slug}/projects', [SkillController::class, 'projects']);
Route::get('/testimonials', [TestimonialController::class, 'index']);
Route::get('/narrator/{context}', [NarratorController::class, 'getText']);
Route::get('/easter-eggs', [EasterEggController::class, 'index']);
Route::post('/easter-eggs/{slug}/validate', [EasterEggController::class, 'validate']);