diff --git a/api/app/Http/Controllers/Api/EasterEggController.php b/api/app/Http/Controllers/Api/EasterEggController.php
new file mode 100644
index 0000000..777ea40
--- /dev/null
+++ b/api/app/Http/Controllers/Api/EasterEggController.php
@@ -0,0 +1,57 @@
+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,
+ ],
+ ]);
+ }
+}
diff --git a/api/app/Models/EasterEgg.php b/api/app/Models/EasterEgg.php
new file mode 100644
index 0000000..5f99e8a
--- /dev/null
+++ b/api/app/Models/EasterEgg.php
@@ -0,0 +1,39 @@
+ '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);
+ }
+}
diff --git a/api/database/migrations/2026_02_08_000001_create_easter_eggs_table.php b/api/database/migrations/2026_02_08_000001_create_easter_eggs_table.php
new file mode 100644
index 0000000..6811cba
--- /dev/null
+++ b/api/database/migrations/2026_02_08_000001_create_easter_eggs_table.php
@@ -0,0 +1,31 @@
+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');
+ }
+};
diff --git a/api/database/seeders/DatabaseSeeder.php b/api/database/seeders/DatabaseSeeder.php
index e295add..8550aa4 100644
--- a/api/database/seeders/DatabaseSeeder.php
+++ b/api/database/seeders/DatabaseSeeder.php
@@ -18,6 +18,7 @@ class DatabaseSeeder extends Seeder
SkillProjectSeeder::class,
TestimonialSeeder::class,
NarratorTextSeeder::class,
+ EasterEggSeeder::class,
]);
}
}
diff --git a/api/database/seeders/EasterEggSeeder.php b/api/database/seeders/EasterEggSeeder.php
new file mode 100644
index 0000000..79bd0db
--- /dev/null
+++ b/api/database/seeders/EasterEggSeeder.php
@@ -0,0 +1,155 @@
+ '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']]
+ );
+ }
+ }
+}
diff --git a/api/database/seeders/NarratorTextSeeder.php b/api/database/seeders/NarratorTextSeeder.php
index 60ae09d..6570bc8 100644
--- a/api/database/seeders/NarratorTextSeeder.php
+++ b/api/database/seeders/NarratorTextSeeder.php
@@ -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) {
diff --git a/api/routes/api.php b/api/routes/api.php
index cf4bd6a..82eb96f 100644
--- a/api/routes/api.php
+++ b/api/routes/api.php
@@ -1,5 +1,6 @@
+
+
+
+
+
+
+
+
*
+
+
+ {{ $t('challenge.success') }}
+
+
+
+ {{ $t('challenge.successMessage') }}
+
+
+
+ {{ $t('challenge.redirecting') }}
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/ChoiceCard.vue b/frontend/app/components/feature/ChoiceCard.vue
new file mode 100644
index 0000000..52cb51c
--- /dev/null
+++ b/frontend/app/components/feature/ChoiceCard.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+ {{ choice.icon }}
+
+
+
+
+ {{ text }}
+
+
+
+
+ ✓
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/ChoiceCards.vue b/frontend/app/components/feature/ChoiceCards.vue
new file mode 100644
index 0000000..0426e78
--- /dev/null
+++ b/frontend/app/components/feature/ChoiceCards.vue
@@ -0,0 +1,147 @@
+
+
+
+
+ {{ question }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/CodePuzzle.vue b/frontend/app/components/feature/CodePuzzle.vue
new file mode 100644
index 0000000..ed0cfb0
--- /dev/null
+++ b/frontend/app/components/feature/CodePuzzle.vue
@@ -0,0 +1,234 @@
+
+
+
+
+
+
+
{{ index + 1 }}
+
+
+
::
+
+
+
{{ line }}
+
+
+
+
+ ^
+
+
+ v
+
+
+
+
+
+
+
+
+ {{ $t('challenge.hintLabel') }}:
+ {{ currentHint }}
+
+
+
+
+
+
+
+ {{ isValidating ? $t('challenge.validating') : $t('challenge.validate') }}
+
+
+
+
+ {{ $t('challenge.needHint') }} ({{ props.hintsUsed }}/3)
+
+
+
+
+
+
+ {{ $t('challenge.wrongOrder') }}
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/CodeWorld.vue b/frontend/app/components/feature/CodeWorld.vue
new file mode 100644
index 0000000..be9b907
--- /dev/null
+++ b/frontend/app/components/feature/CodeWorld.vue
@@ -0,0 +1,89 @@
+
+
+
{{ visibleCode }}
+
+
+
+ {{ $t('revelation.codeWorldAlt') }}
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/EasterEggCollection.vue b/frontend/app/components/feature/EasterEggCollection.vue
new file mode 100644
index 0000000..d8bb028
--- /dev/null
+++ b/frontend/app/components/feature/EasterEggCollection.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+ {{ $t('easterEgg.collection') }}
+
+
+ {{ foundCount }}
+ /
+ {{ totalEasterEggs }}
+
+
+
+
+
+
*
+
+ {{ $t('easterEgg.allFound') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ isFound(egg.slug) ? getTriggerIcon(egg.trigger_type) : '?' }}
+
+
+
+
+ {{ isFound(egg.slug) ? formatSlug(egg.slug) : '???' }}
+
+
+
+
+ {{ getDifficultyStars(egg.difficulty) }}
+
+
+
+
+
+
+ {{ $t('easterEgg.hint') }}
+
+
+
+
+
diff --git a/frontend/app/components/feature/EasterEggNotification.vue b/frontend/app/components/feature/EasterEggNotification.vue
new file mode 100644
index 0000000..52fff4b
--- /dev/null
+++ b/frontend/app/components/feature/EasterEggNotification.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
*
+
+
+ {{ $t('easterEgg.found') }}
+
+
+ {{ $t('easterEgg.count', { found: foundCount, total: totalEasterEggs }) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/EasterEggPopup.vue b/frontend/app/components/feature/EasterEggPopup.vue
new file mode 100644
index 0000000..b738772
--- /dev/null
+++ b/frontend/app/components/feature/EasterEggPopup.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+ *
+
+
+
+
+ {{ rewardIcon }}
+
+
+
+
+ {{ $t('easterEgg.found') }}
+
+
+
+
+ {{ $t('easterEgg.count', { found: foundCount, total: totalEasterEggs }) }}
+
+
+
+
+
+
{{ reward.reward }}
+
+
+
+ {{ reward.reward }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('easterEgg.difficulty') }}:
+
+ *
+
+
+
+
+
+ {{ $t('common.continue') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/IntroBackground.vue b/frontend/app/components/feature/IntroBackground.vue
new file mode 100644
index 0000000..0026fa0
--- /dev/null
+++ b/frontend/app/components/feature/IntroBackground.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+ {{ codeSymbols[(i - 1) % codeSymbols.length] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/IntroSequence.vue b/frontend/app/components/feature/IntroSequence.vue
new file mode 100644
index 0000000..54c3a1b
--- /dev/null
+++ b/frontend/app/components/feature/IntroSequence.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+ {{ displayedText }}
+
+
+
+
+
+ {{ $t('narrator.clickToSkip') }}
+
+
+
+
+
+
+
+
diff --git a/frontend/app/components/feature/ZoneEndChoice.vue b/frontend/app/components/feature/ZoneEndChoice.vue
new file mode 100644
index 0000000..08f8e6a
--- /dev/null
+++ b/frontend/app/components/feature/ZoneEndChoice.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+ {{ locale === 'fr' ? choicePoint.questionFr : choicePoint.questionEn }}
+
+
+
+ {{ choicePoint.choices[0].icon }}
+ {{ locale === 'fr' ? choicePoint.choices[0].textFr : choicePoint.choices[0].textEn }}
+
+
+
+
+
+
+
diff --git a/frontend/app/composables/useEasterEggDetection.ts b/frontend/app/composables/useEasterEggDetection.ts
new file mode 100644
index 0000000..8a23790
--- /dev/null
+++ b/frontend/app/composables/useEasterEggDetection.ts
@@ -0,0 +1,174 @@
+import type { EasterEggMeta } from './useFetchEasterEggs'
+
+interface UseEasterEggDetectionOptions {
+ onFound: (slug: string) => void
+}
+
+// Konami Code
+const KONAMI_CODE = [
+ 'ArrowUp',
+ 'ArrowUp',
+ 'ArrowDown',
+ 'ArrowDown',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'KeyB',
+ 'KeyA',
+]
+
+export function useEasterEggDetection(options: UseEasterEggDetectionOptions) {
+ const { fetchList } = useFetchEasterEggs()
+ const progressionStore = useProgressionStore()
+
+ // State
+ const konamiIndex = ref(0)
+ const clickSequence = ref([])
+ let konamiListenerAdded = false
+
+ // Load easter eggs on mount
+ onMounted(() => {
+ fetchList()
+ initKonamiListener()
+ })
+
+ // === Konami Code ===
+ function initKonamiListener() {
+ if (import.meta.client && !konamiListenerAdded) {
+ window.addEventListener('keydown', handleKonamiKey)
+ konamiListenerAdded = true
+ }
+ }
+
+ function handleKonamiKey(e: KeyboardEvent) {
+ if (e.code === KONAMI_CODE[konamiIndex.value]) {
+ konamiIndex.value++
+ if (konamiIndex.value === KONAMI_CODE.length) {
+ triggerEasterEgg('konami-master')
+ konamiIndex.value = 0
+ }
+ } else {
+ konamiIndex.value = 0
+ }
+ }
+
+ // === Click Detection ===
+ function detectClick(targetSlug: string, requiredClicks: number = 1) {
+ const clicks = ref(0)
+
+ function handleClick() {
+ clicks.value++
+ if (clicks.value >= requiredClicks) {
+ triggerEasterEgg(targetSlug)
+ clicks.value = 0
+ }
+ }
+
+ return { handleClick, clicks }
+ }
+
+ // === Hover Detection ===
+ function detectHover(targetSlug: string, hoverTime: number = 2000) {
+ let timeoutId: ReturnType | null = null
+
+ function handleMouseEnter() {
+ timeoutId = setTimeout(() => {
+ triggerEasterEgg(targetSlug)
+ }, hoverTime)
+ }
+
+ function handleMouseLeave() {
+ if (timeoutId) {
+ clearTimeout(timeoutId)
+ timeoutId = null
+ }
+ }
+
+ return { handleMouseEnter, handleMouseLeave }
+ }
+
+ // === Scroll Detection ===
+ function detectScrollBottom(targetSlug: string) {
+ let triggered = false
+
+ function checkScroll() {
+ if (triggered) return
+
+ const scrollTop = window.scrollY
+ const windowHeight = window.innerHeight
+ const docHeight = document.documentElement.scrollHeight
+
+ if (scrollTop + windowHeight >= docHeight - 50) {
+ triggerEasterEgg(targetSlug)
+ triggered = true
+ }
+ }
+
+ onMounted(() => {
+ if (import.meta.client) {
+ window.addEventListener('scroll', checkScroll, { passive: true })
+ }
+ })
+
+ onUnmounted(() => {
+ if (import.meta.client) {
+ window.removeEventListener('scroll', checkScroll)
+ }
+ })
+ }
+
+ // === Sequence Detection ===
+ function detectSequence(expectedSequence: string[], targetSlug: string) {
+ function addToSequence(item: string) {
+ clickSequence.value.push(item)
+
+ // Keep only the last N items
+ if (clickSequence.value.length > expectedSequence.length) {
+ clickSequence.value.shift()
+ }
+
+ // Check if sequence matches
+ if (clickSequence.value.length === expectedSequence.length) {
+ const match = clickSequence.value.every(
+ (val, idx) => val === expectedSequence[idx]
+ )
+ if (match) {
+ triggerEasterEgg(targetSlug)
+ clickSequence.value = []
+ }
+ }
+ }
+
+ return { addToSequence }
+ }
+
+ // === Trigger Easter Egg ===
+ function triggerEasterEgg(slug: string) {
+ // Check if already found
+ if (progressionStore.easterEggsFound.includes(slug)) {
+ return
+ }
+
+ // Mark as found
+ progressionStore.markEasterEggFound(slug)
+
+ // Notify
+ options.onFound(slug)
+ }
+
+ onUnmounted(() => {
+ if (import.meta.client && konamiListenerAdded) {
+ window.removeEventListener('keydown', handleKonamiKey)
+ konamiListenerAdded = false
+ }
+ })
+
+ return {
+ detectClick,
+ detectHover,
+ detectScrollBottom,
+ detectSequence,
+ triggerEasterEgg,
+ }
+}
diff --git a/frontend/app/composables/useFetchEasterEggs.ts b/frontend/app/composables/useFetchEasterEggs.ts
new file mode 100644
index 0000000..f5fc4d9
--- /dev/null
+++ b/frontend/app/composables/useFetchEasterEggs.ts
@@ -0,0 +1,83 @@
+export type TriggerType = 'click' | 'hover' | 'konami' | 'scroll' | 'sequence'
+export type RewardType = 'snippet' | 'anecdote' | 'image' | 'badge'
+
+export interface EasterEggMeta {
+ slug: string
+ location: string
+ trigger_type: TriggerType
+ difficulty: number
+}
+
+export interface EasterEggReward {
+ slug: string
+ reward_type: RewardType
+ reward: string
+ difficulty: number
+}
+
+interface EasterEggListResponse {
+ data: EasterEggMeta[]
+ meta: { total: number }
+}
+
+interface EasterEggValidateResponse {
+ data: EasterEggReward
+}
+
+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
+
+ try {
+ const response = await $fetch('/easter-eggs', {
+ baseURL: config.public.apiUrl as string,
+ headers: {
+ 'X-API-Key': config.public.apiKey as string,
+ },
+ })
+
+ availableEasterEggs.value = response.data
+ isLoaded.value = true
+ return response.data
+ } catch {
+ return []
+ }
+ }
+
+ async function validate(slug: string): Promise {
+ try {
+ const response = await $fetch(`/easter-eggs/${slug}/validate`, {
+ method: 'POST',
+ baseURL: config.public.apiUrl as string,
+ headers: {
+ 'X-API-Key': config.public.apiKey as string,
+ 'Accept-Language': locale.value,
+ },
+ })
+ return response.data
+ } catch {
+ return null
+ }
+ }
+
+ function getByLocation(location: string): EasterEggMeta[] {
+ return availableEasterEggs.value.filter(
+ e => e.location === location || e.location === 'global'
+ )
+ }
+
+ return {
+ availableEasterEggs: readonly(availableEasterEggs),
+ isLoaded: readonly(isLoaded),
+ fetchList,
+ validate,
+ getByLocation,
+ }
+}
diff --git a/frontend/app/composables/useFetchNarratorText.ts b/frontend/app/composables/useFetchNarratorText.ts
index 3dc1b1c..a8782a7 100644
--- a/frontend/app/composables/useFetchNarratorText.ts
+++ b/frontend/app/composables/useFetchNarratorText.ts
@@ -1,5 +1,8 @@
export type NarratorContext =
| 'intro'
+ | 'intro_sequence_1'
+ | 'intro_sequence_2'
+ | 'intro_sequence_3'
| 'transition_projects'
| 'transition_skills'
| 'transition_testimonials'
diff --git a/frontend/app/composables/useNarrativePath.ts b/frontend/app/composables/useNarrativePath.ts
new file mode 100644
index 0000000..4297cb7
--- /dev/null
+++ b/frontend/app/composables/useNarrativePath.ts
@@ -0,0 +1,153 @@
+import { ZONES, NARRATIVE_PATHS, choiceIdToZone, type ZoneKey } from '~/data/narrativePaths'
+import type { Choice, ChoicePoint } from '~/types/choice'
+
+export function useNarrativePath() {
+ const progressionStore = useProgressionStore()
+ const { locale } = useI18n()
+ const route = useRoute()
+
+ // Déterminer le chemin actuel basé sur les choix
+ const currentPath = computed(() => {
+ const choices = progressionStore.choices
+
+ // Premier choix (intro)
+ const introChoice = choices['intro_first_choice']
+ if (!introChoice) return null
+
+ const startZone = choiceIdToZone(introChoice)
+ if (!startZone) return null
+
+ // Filtrer les chemins qui commencent par cette zone
+ let possiblePaths = NARRATIVE_PATHS.filter(path => path[0] === startZone)
+
+ // Affiner avec les choix suivants si disponibles
+ const afterFirstZoneChoice = choices[`after_${startZone}`]
+ if (afterFirstZoneChoice && possiblePaths.length > 1) {
+ const secondZone = choiceIdToZone(afterFirstZoneChoice)
+ if (secondZone) {
+ possiblePaths = possiblePaths.filter(path => path[1] === secondZone)
+ }
+ }
+
+ return possiblePaths[0] || null
+ })
+
+ // Zone actuelle basée sur la route
+ const currentZone = computed(() => {
+ const path = route.path.toLowerCase()
+
+ if (path.includes('projets') || path.includes('projects')) return 'projects'
+ if (path.includes('competences') || path.includes('skills')) return 'skills'
+ if (path.includes('temoignages') || path.includes('testimonials')) return 'testimonials'
+ if (path.includes('parcours') || path.includes('journey')) return 'journey'
+ if (path.includes('contact')) return 'contact'
+
+ return null
+ })
+
+ // Index de la zone actuelle dans le chemin
+ const currentZoneIndex = computed(() => {
+ if (!currentPath.value || !currentZone.value) return -1
+ return currentPath.value.indexOf(currentZone.value)
+ })
+
+ // Prochaine zone suggérée
+ const suggestedNextZone = computed(() => {
+ if (!currentPath.value || currentZoneIndex.value === -1) return null
+
+ const nextIndex = currentZoneIndex.value + 1
+ if (nextIndex >= currentPath.value.length) return null
+
+ return currentPath.value[nextIndex]
+ })
+
+ // Zones restantes à visiter (excluant contact)
+ const remainingZones = computed(() => {
+ const mainZones: ZoneKey[] = ['projects', 'skills', 'testimonials', 'journey']
+ const visited = progressionStore.visitedSections
+
+ return mainZones.filter(zone => !visited.includes(zone))
+ })
+
+ // Obtenir la route pour une zone
+ function getZoneRoute(zone: ZoneKey): string {
+ const zoneInfo = ZONES[zone]
+ if (!zoneInfo) return '/'
+ return locale.value === 'fr' ? zoneInfo.routeFr : zoneInfo.routeEn
+ }
+
+ // Générer le ChoicePoint pour après la zone actuelle
+ function getNextChoicePoint(): ChoicePoint | null {
+ if (remainingZones.value.length === 0) {
+ // Plus de zones, proposer le contact uniquement
+ return {
+ id: 'go_to_contact',
+ questionFr: 'Tu as tout exploré ! Prêt à me rencontrer ?',
+ questionEn: 'You explored everything! Ready to meet me?',
+ choices: [
+ createChoice('contact'),
+ createChoice('contact'), // Dupliqué car ChoicePoint attend 2 choices
+ ],
+ context: 'contact_ready',
+ }
+ }
+
+ // Proposer les 2 prochaines zones non visitées
+ const nextTwo = remainingZones.value.slice(0, 2)
+
+ if (nextTwo.length === 1) {
+ // Une seule zone restante, proposer zone + contact
+ return {
+ id: `after_${currentZone.value}`,
+ questionFr: 'Où vas-tu ensuite ?',
+ questionEn: 'Where to next?',
+ choices: [
+ createChoice(nextTwo[0]),
+ createChoice('contact'),
+ ],
+ context: 'one_zone_left',
+ }
+ }
+
+ return {
+ id: `after_${currentZone.value}`,
+ questionFr: 'Où vas-tu ensuite ?',
+ questionEn: 'Where to next?',
+ choices: [
+ createChoice(nextTwo[0]),
+ createChoice(nextTwo[1]),
+ ],
+ context: 'zone_choice',
+ }
+ }
+
+ // Créer un objet Choice pour une zone
+ function createChoice(zone: ZoneKey): Choice {
+ const zoneInfo = ZONES[zone]
+ return {
+ id: `choice_${zone}`,
+ textFr: zoneInfo.labelFr,
+ textEn: zoneInfo.labelEn,
+ icon: zoneInfo.icon,
+ destination: getZoneRoute(zone),
+ zoneColor: zoneInfo.color,
+ }
+ }
+
+ // Vérifier si le contact est débloqué
+ const isContactReady = computed(() => {
+ return remainingZones.value.length === 0 || progressionStore.isContactUnlocked
+ })
+
+ return {
+ currentPath,
+ currentZone,
+ currentZoneIndex,
+ suggestedNextZone,
+ remainingZones,
+ isContactReady,
+ getZoneRoute,
+ getNextChoicePoint,
+ createChoice,
+ }
+}
diff --git a/frontend/app/data/narrativePaths.ts b/frontend/app/data/narrativePaths.ts
new file mode 100644
index 0000000..de5ea70
--- /dev/null
+++ b/frontend/app/data/narrativePaths.ts
@@ -0,0 +1,85 @@
+export type ZoneKey = 'projects' | 'skills' | 'testimonials' | 'journey' | 'contact'
+
+export interface ZoneInfo {
+ labelFr: string
+ labelEn: string
+ icon: string
+ color: string
+ routeFr: string
+ routeEn: string
+}
+
+export const ZONES: Record = {
+ projects: {
+ labelFr: 'Découvrir les créations',
+ labelEn: 'Discover the creations',
+ icon: '💻',
+ color: '#3b82f6',
+ routeFr: '/projets',
+ routeEn: '/en/projects',
+ },
+ skills: {
+ labelFr: 'Explorer les compétences',
+ labelEn: 'Explore the skills',
+ icon: '⚡',
+ color: '#10b981',
+ routeFr: '/competences',
+ routeEn: '/en/skills',
+ },
+ testimonials: {
+ labelFr: 'Écouter les témoignages',
+ labelEn: 'Listen to testimonials',
+ icon: '💬',
+ color: '#f59e0b',
+ routeFr: '/temoignages',
+ routeEn: '/en/testimonials',
+ },
+ journey: {
+ labelFr: 'Suivre le parcours',
+ labelEn: 'Follow the journey',
+ icon: '📍',
+ color: '#8b5cf6',
+ routeFr: '/parcours',
+ routeEn: '/en/journey',
+ },
+ contact: {
+ labelFr: 'Rencontrer le développeur',
+ labelEn: 'Meet the developer',
+ icon: '📧',
+ color: '#fa784f',
+ routeFr: '/contact',
+ routeEn: '/en/contact',
+ },
+}
+
+// Chemins possibles (8 combinaisons basées sur 3 points de choix)
+// Tous les chemins passent par toutes les zones et mènent au contact
+export const NARRATIVE_PATHS: ZoneKey[][] = [
+ // Chemin 1-4 : Commençant par Projets
+ ['projects', 'testimonials', 'skills', 'journey', 'contact'],
+ ['projects', 'testimonials', 'journey', 'skills', 'contact'],
+ ['projects', 'journey', 'testimonials', 'skills', 'contact'],
+ ['projects', 'journey', 'skills', 'testimonials', 'contact'],
+ // Chemin 5-8 : Commençant par Compétences
+ ['skills', 'testimonials', 'projects', 'journey', 'contact'],
+ ['skills', 'testimonials', 'journey', 'projects', 'contact'],
+ ['skills', 'journey', 'testimonials', 'projects', 'contact'],
+ ['skills', 'journey', 'projects', 'testimonials', 'contact'],
+]
+
+// Points de choix et leurs options
+export const CHOICE_POINT_OPTIONS: Record = {
+ intro_first_choice: { optionA: 'projects', optionB: 'skills' },
+ after_projects: { optionA: 'testimonials', optionB: 'journey' },
+ after_skills: { optionA: 'testimonials', optionB: 'journey' },
+}
+
+// Mapper choice ID vers zone
+export function choiceIdToZone(choiceId: string): ZoneKey | null {
+ if (choiceId.includes('projects')) return 'projects'
+ if (choiceId.includes('skills')) return 'skills'
+ if (choiceId.includes('testimonials')) return 'testimonials'
+ if (choiceId.includes('journey')) return 'journey'
+ if (choiceId.includes('contact')) return 'contact'
+ return null
+}
diff --git a/frontend/app/pages/challenge.vue b/frontend/app/pages/challenge.vue
new file mode 100644
index 0000000..b457e15
--- /dev/null
+++ b/frontend/app/pages/challenge.vue
@@ -0,0 +1,134 @@
+
+
+
+
+ {{ $t('challenge.skip') }}
+
+
+
+
+
+
+
+ ?
+
+
+
+ {{ $t('challenge.title') }}
+
+
+
+ {{ $t('challenge.intro') }}
+
+
+
+ {{ $t('challenge.accept') }}
+
+
+
+
+
+
+
+
+ {{ $t('challenge.puzzleTitle') }}
+
+
+
+ {{ $t('challenge.puzzleInstruction') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/pages/competences.vue b/frontend/app/pages/competences.vue
index 7a85ea4..d09df61 100644
--- a/frontend/app/pages/competences.vue
+++ b/frontend/app/pages/competences.vue
@@ -67,6 +67,9 @@
+
+
+
-
-
{{ $t('pages.contact.title') }}
-
{{ $t('pages.contact.description') }}
+
+
+
+
+ {{ $t('contact.statsTitle') }}
+
+
+
+
{{ stats.zonesVisited }}/{{ stats.zonesTotal }}
+
{{ $t('contact.zones') }}
+
+
+
{{ stats.easterEggsFound }}/{{ stats.easterEggsTotal }}
+
{{ $t('contact.easterEggs') }}
+
+
+
+ {{ stats.challengeCompleted ? 'OK' : '-' }}
+
+
{{ $t('contact.challenge') }}
+
+
+
+
+
+
+
+ {{ $t('contact.title') }}
+
+
+ {{ $t('contact.subtitle') }}
+
+
+
+
+
+
+
!
+
+ {{ $t('contact.success') }}
+
+
+ {{ $t('contact.successMessage') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/pages/index.vue b/frontend/app/pages/index.vue
index e828897..de76621 100644
--- a/frontend/app/pages/index.vue
+++ b/frontend/app/pages/index.vue
@@ -62,6 +62,6 @@ function onHeroConfirm() {
if (selectedHero.value) {
store.setHero(selectedHero.value)
}
- navigateTo(localePath('/projets'))
+ navigateTo(localePath('/intro'))
}
diff --git a/frontend/app/pages/intro.vue b/frontend/app/pages/intro.vue
new file mode 100644
index 0000000..92cfe53
--- /dev/null
+++ b/frontend/app/pages/intro.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isLastTextStep ? $t('intro.startExploring') : $t('intro.continue') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('intro.skip') }}
+
+
+
+
+
+
+
+
diff --git a/frontend/app/pages/parcours.vue b/frontend/app/pages/parcours.vue
index f1d01e0..f264cfd 100644
--- a/frontend/app/pages/parcours.vue
+++ b/frontend/app/pages/parcours.vue
@@ -43,6 +43,9 @@
+
+
+
@@ -55,7 +58,7 @@ definePageMeta({
const { setPageMeta } = useSeo()
const { t, tm } = useI18n()
-const progressStore = useProgressStore()
+const progressStore = useProgressionStore()
setPageMeta({
title: t('journey.page_title'),
diff --git a/frontend/app/pages/projets/index.vue b/frontend/app/pages/projets/index.vue
index 83574ba..cd8812f 100644
--- a/frontend/app/pages/projets/index.vue
+++ b/frontend/app/pages/projets/index.vue
@@ -48,6 +48,9 @@
:style="{ '--animation-delay': `${index * 80}ms` }"
/>
+
+
+
diff --git a/frontend/app/pages/revelation.vue b/frontend/app/pages/revelation.vue
new file mode 100644
index 0000000..e3dd504
--- /dev/null
+++ b/frontend/app/pages/revelation.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
...
+
{{ $t('revelation.loading') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('revelation.bugSays') }}
+
+
+
+
+ {{ $t('revelation.foundMe') }}
+
+
+
+ {{ $t('revelation.message') }}
+
+
+
+
+ - Celian
+
+
+
+
+ {{ $t('revelation.contactCta') }}
+ ->
+
+
+
+
+
+
+ {{ $t('revelation.srDescription') }}
+
+
+
+
+
+
+
diff --git a/frontend/app/pages/temoignages.vue b/frontend/app/pages/temoignages.vue
index 2a46a7c..0d4c8d4 100644
--- a/frontend/app/pages/temoignages.vue
+++ b/frontend/app/pages/temoignages.vue
@@ -97,22 +97,8 @@
-
-
-
- {{ $t('testimonials.cta_title') }}
-
-
- {{ $t('testimonials.cta_description') }}
-
-
- {{ $t('testimonials.cta_button') }}
-
-
-
+
+
@@ -125,7 +111,7 @@ definePageMeta({
const { setPageMeta } = useSeo()
const { t } = useI18n()
-const progressStore = useProgressStore()
+const progressStore = useProgressionStore()
setPageMeta({
title: t('testimonials.page_title'),
diff --git a/frontend/app/stores/progression.ts b/frontend/app/stores/progression.ts
index 86221fd..159ff06 100644
--- a/frontend/app/stores/progression.ts
+++ b/frontend/app/stores/progression.ts
@@ -13,6 +13,7 @@ export interface ProgressionState {
contactUnlocked: boolean
choices: Record
consentGiven: boolean | null
+ introSeen: boolean
}
const NARRATOR_STAGE_THRESHOLDS = [0, 20, 40, 60, 80]
@@ -81,6 +82,7 @@ export const useProgressionStore = defineStore('progression', {
contactUnlocked: false,
choices: {},
consentGiven: null,
+ introSeen: false,
}),
getters: {
@@ -93,6 +95,8 @@ export const useProgressionStore = defineStore('progression', {
hasExistingProgress: (state) => state.visitedSections.length > 0 || state.hero !== null,
narratorStage: (state) => calculateNarratorStage(state.completionPercent),
+
+ easterEggsFoundCount: (state) => state.easterEggsFound.length,
},
actions: {
@@ -124,6 +128,10 @@ export const useProgressionStore = defineStore('progression', {
}
},
+ markEasterEggFound(slug: string) {
+ this.findEasterEgg(slug)
+ },
+
completeChallenge() {
this.challengeCompleted = true
},
@@ -136,6 +144,10 @@ export const useProgressionStore = defineStore('progression', {
this.choices[choiceId] = value
},
+ setIntroSeen(seen: boolean) {
+ this.introSeen = seen
+ },
+
setConsent(given: boolean) {
this.consentGiven = given
if (given) {
diff --git a/frontend/app/types/choice.ts b/frontend/app/types/choice.ts
new file mode 100644
index 0000000..acbbc78
--- /dev/null
+++ b/frontend/app/types/choice.ts
@@ -0,0 +1,67 @@
+export interface Choice {
+ id: string
+ textFr: string
+ textEn: string
+ icon: string
+ destination: string
+ zoneColor: string
+}
+
+export interface ChoicePoint {
+ id: string
+ questionFr: string
+ questionEn: string
+ choices: [Choice, Choice]
+ context: string
+}
+
+export const CHOICE_POINTS: Record = {
+ intro_first_choice: {
+ id: 'intro_first_choice',
+ questionFr: 'Par où veux-tu commencer ton exploration ?',
+ questionEn: 'Where do you want to start your exploration?',
+ choices: [
+ {
+ id: 'choice_projects_first',
+ textFr: 'Découvrir les créations',
+ textEn: 'Discover the creations',
+ icon: '💻',
+ destination: '/projets',
+ zoneColor: '#3b82f6',
+ },
+ {
+ id: 'choice_skills_first',
+ textFr: 'Explorer les compétences',
+ textEn: 'Explore the skills',
+ icon: '⚡',
+ destination: '/competences',
+ zoneColor: '#10b981',
+ },
+ ],
+ context: 'intro',
+ },
+ after_projects: {
+ id: 'after_projects',
+ questionFr: 'Quelle sera ta prochaine étape ?',
+ questionEn: 'What will be your next step?',
+ choices: [
+ {
+ id: 'choice_testimonials',
+ textFr: "Écouter ceux qui l'ont rencontré",
+ textEn: 'Listen to those who met him',
+ icon: '💬',
+ destination: '/temoignages',
+ zoneColor: '#f59e0b',
+ },
+ {
+ id: 'choice_journey',
+ textFr: 'Suivre son parcours',
+ textEn: 'Follow his journey',
+ icon: '📍',
+ destination: '/parcours',
+ zoneColor: '#8b5cf6',
+ },
+ ],
+ context: 'after_projects',
+ },
+}
diff --git a/frontend/i18n/en.json b/frontend/i18n/en.json
index 72ec6ef..52540be 100644
--- a/frontend/i18n/en.json
+++ b/frontend/i18n/en.json
@@ -179,6 +179,16 @@
"clickToSkip": "Click or press Space to skip",
"bugAlt": "The Bug - Stage {stage}"
},
+ "intro": {
+ "continue": "Continue",
+ "startExploring": "Start exploring",
+ "skip": "Skip intro",
+ "fallback": {
+ "seq1": "Welcome to my domain... I am The Bug, your guide for this adventure.",
+ "seq2": "There's someone here you're looking for... A mysterious developer who created everything you see.",
+ "seq3": "To find them, explore this world. Each zone hides a part of their story. Are you ready?"
+ }
+ },
"map": {
"ariaLabel": "Interactive portfolio map. Use Tab to navigate between zones and Enter to explore.",
"instructions": "Use Tab keys to navigate between zones and Enter or Space to explore a zone.",
@@ -252,5 +262,63 @@
"title": "Quick Resume",
"description": "The essentials at a glance"
}
+ },
+ "easterEgg": {
+ "found": "Easter Egg found!",
+ "count": "{found} / {total} discovered",
+ "difficulty": "Difficulty",
+ "collection": "My Collection",
+ "allFound": "Collection complete! You're a true explorer!",
+ "hint": "Keep exploring... surprises are hidden everywhere!"
+ },
+ "challenge": {
+ "title": "One last challenge...",
+ "intro": "Before meeting the developer, prove you understand the basics of code. Nothing too hard, I promise.",
+ "accept": "Accept the challenge",
+ "skip": "Skip to contact",
+ "puzzleTitle": "Put the code in order",
+ "puzzleInstruction": "Drag the lines to reconstruct the function that unlocks access to the developer.",
+ "hint1": "The function starts with function unlockDeveloper() {",
+ "hint2": "The secret variable is defined right after the opening brace",
+ "hint3": "The last line before the closing brace is return Keep exploring...",
+ "hintLabel": "Hint",
+ "needHint": "Need help?",
+ "validate": "Check",
+ "validating": "Checking...",
+ "wrongOrder": "That's not the right order... Try again!",
+ "moveUp": "Move up",
+ "moveDown": "Move down",
+ "success": "Well done!",
+ "successMessage": "You've proven your worth. The path to the developer is now open...",
+ "redirecting": "Redirecting..."
+ },
+ "revelation": {
+ "loading": "Loading...",
+ "bugSays": "The Bug says: You found them!",
+ "foundMe": "You found me!",
+ "message": "Welcome to my world of code. Thank you for exploring all this way to find me.",
+ "contactCta": "Contact me",
+ "srDescription": "Revelation page. You found the developer Celian. A button allows you to contact them.",
+ "codeWorldAlt": "An ASCII art world representing a code landscape with the message You found me in the center."
+ },
+ "contact": {
+ "statsTitle": "Your journey",
+ "zones": "Zones visited",
+ "easterEggs": "Easter eggs",
+ "challenge": "Challenge",
+ "title": "Contact me",
+ "subtitle": "Congratulations for exploring all this way! I'd be happy to chat with you.",
+ "name": "Name",
+ "namePlaceholder": "Your name",
+ "email": "Email",
+ "emailPlaceholder": "your.email(at)example.com",
+ "message": "Message",
+ "messagePlaceholder": "What brings you here?",
+ "send": "Send",
+ "sending": "Sending...",
+ "success": "Message sent!",
+ "successMessage": "Thanks for your message. I'll get back to you as soon as possible.",
+ "error": "An error occurred. Please try again later.",
+ "rateLimitError": "Too many attempts. Please wait a moment before trying again."
}
}
diff --git a/frontend/i18n/fr.json b/frontend/i18n/fr.json
index 72e7f61..a7fa681 100644
--- a/frontend/i18n/fr.json
+++ b/frontend/i18n/fr.json
@@ -179,6 +179,16 @@
"clickToSkip": "Cliquez ou appuyez sur Espace pour passer",
"bugAlt": "Le Bug - Stade {stage}"
},
+ "intro": {
+ "continue": "Continuer",
+ "startExploring": "Commencer l'exploration",
+ "skip": "Passer l'intro",
+ "fallback": {
+ "seq1": "Bienvenue dans mon domaine... Je suis Le Bug, ton guide pour cette aventure.",
+ "seq2": "Il y a quelqu'un ici que tu cherches... Un développeur mystérieux qui a créé tout ce que tu vois.",
+ "seq3": "Pour le trouver, explore ce monde. Chaque zone cache une partie de son histoire. Es-tu prêt ?"
+ }
+ },
"map": {
"ariaLabel": "Carte interactive du portfolio. Utilisez Tab pour naviguer entre les zones et Entrée pour explorer.",
"instructions": "Utilisez les touches Tab pour naviguer entre les zones et Entrée ou Espace pour explorer une zone.",
@@ -252,5 +262,63 @@
"title": "Résumé Express",
"description": "L'essentiel en un coup d'œil"
}
+ },
+ "easterEgg": {
+ "found": "Easter Egg trouve !",
+ "count": "{found} / {total} decouverts",
+ "difficulty": "Difficulte",
+ "collection": "Ma Collection",
+ "allFound": "Collection complete ! Tu es un vrai explorateur !",
+ "hint": "Continue d'explorer... des surprises sont cachees partout !"
+ },
+ "challenge": {
+ "title": "Une derniere epreuve...",
+ "intro": "Avant de rencontrer le developpeur, prouve que tu maitrises les bases du code. Rien de bien mechant, promis.",
+ "accept": "Relever le defi",
+ "skip": "Passer directement au contact",
+ "puzzleTitle": "Remets le code dans l'ordre",
+ "puzzleInstruction": "Glisse les lignes pour reconstituer la fonction qui debloque l'acces au developpeur.",
+ "hint1": "La fonction commence par function unlockDeveloper() {",
+ "hint2": "La variable secret est definie juste apres l'accolade ouvrante",
+ "hint3": "La derniere ligne avant l'accolade fermante est return Keep exploring...",
+ "hintLabel": "Indice",
+ "needHint": "Besoin d'aide ?",
+ "validate": "Verifier",
+ "validating": "Verification...",
+ "wrongOrder": "Ce n'est pas le bon ordre... Essaie encore !",
+ "moveUp": "Monter",
+ "moveDown": "Descendre",
+ "success": "Bravo !",
+ "successMessage": "Tu as prouve ta valeur. Le chemin vers le developpeur est maintenant ouvert...",
+ "redirecting": "Redirection en cours..."
+ },
+ "revelation": {
+ "loading": "Chargement...",
+ "bugSays": "Le Bug dit : Tu l'as trouve !",
+ "foundMe": "Tu m'as trouve !",
+ "message": "Bienvenue dans mon monde de code. Merci d'avoir explore tout ce chemin pour me trouver.",
+ "contactCta": "Me contacter",
+ "srDescription": "Page de revelation. Vous avez trouve le developpeur Celian. Un bouton permet de le contacter.",
+ "codeWorldAlt": "Un monde en ASCII art representant un paysage de code avec le message Tu m'as trouve au centre."
+ },
+ "contact": {
+ "statsTitle": "Ton parcours",
+ "zones": "Zones visitees",
+ "easterEggs": "Easter eggs",
+ "challenge": "Defi",
+ "title": "Me contacter",
+ "subtitle": "Felicitations pour avoir explore tout ce chemin ! Je serais ravi d'echanger avec toi.",
+ "name": "Nom",
+ "namePlaceholder": "Ton nom",
+ "email": "Email",
+ "emailPlaceholder": "ton.email(at)example.com",
+ "message": "Message",
+ "messagePlaceholder": "Qu'est-ce qui t'amene ?",
+ "send": "Envoyer",
+ "sending": "Envoi en cours...",
+ "success": "Message envoye !",
+ "successMessage": "Merci pour ton message. Je te repondrai dans les plus brefs delais.",
+ "error": "Une erreur s'est produite. Reessaie plus tard.",
+ "rateLimitError": "Trop de tentatives. Patiente un moment avant de reessayer."
}
}