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,100 @@
<template>
<div class="intro-background absolute inset-0 overflow-hidden">
<!-- Gradient de fond -->
<div class="absolute inset-0 bg-gradient-to-b from-sky-dark via-sky-dark-50 to-sky-dark" />
<!-- Particules flottantes (code fragments) -->
<div
v-if="!reducedMotion"
class="particles absolute inset-0"
>
<div
v-for="i in 20"
:key="i"
class="particle absolute text-sky-accent/10 font-mono text-xs"
:style="{
left: `${particlePositions[i - 1]?.x ?? 0}%`,
top: `${particlePositions[i - 1]?.y ?? 0}%`,
animationDelay: `${particlePositions[i - 1]?.delay ?? 0}s`,
animationDuration: `${10 + (particlePositions[i - 1]?.duration ?? 0)}s`,
}"
>
{{ codeSymbols[(i - 1) % codeSymbols.length] }}
</div>
</div>
<!-- Toile d'araignée stylisée (SVG) -->
<svg
class="absolute top-0 right-0 w-64 h-64 text-sky-dark-100/30"
viewBox="0 0 200 200"
>
<path
d="M100,100 L100,0 M100,100 L200,100 M100,100 L100,200 M100,100 L0,100 M100,100 L170,30 M100,100 L170,170 M100,100 L30,170 M100,100 L30,30"
stroke="currentColor"
stroke-width="1"
fill="none"
/>
<circle cx="100" cy="100" r="30" stroke="currentColor" stroke-width="1" fill="none" />
<circle cx="100" cy="100" r="60" stroke="currentColor" stroke-width="1" fill="none" />
<circle cx="100" cy="100" r="90" stroke="currentColor" stroke-width="1" fill="none" />
</svg>
<!-- Toile en bas à gauche -->
<svg
class="absolute bottom-0 left-0 w-48 h-48 text-sky-dark-100/20 transform rotate-180"
viewBox="0 0 200 200"
>
<path
d="M100,100 L100,0 M100,100 L200,100 M100,100 L170,30 M100,100 L170,170"
stroke="currentColor"
stroke-width="1"
fill="none"
/>
<circle cx="100" cy="100" r="40" stroke="currentColor" stroke-width="1" fill="none" />
<circle cx="100" cy="100" r="80" stroke="currentColor" stroke-width="1" fill="none" />
</svg>
</div>
</template>
<script setup lang="ts">
const reducedMotion = useReducedMotion()
const codeSymbols = ['</', '/>', '{}', '[]', '()', '=>', '&&', '||']
// Générer des positions aléatoires côté serveur-safe
const particlePositions = Array.from({ length: 20 }, (_, i) => ({
x: ((i * 17 + 7) % 100),
y: ((i * 23 + 13) % 100),
delay: (i * 0.3) % 5,
duration: (i % 10),
}))
</script>
<style scoped>
@keyframes float-up {
from {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
to {
transform: translateY(-100vh) rotate(360deg);
opacity: 0;
}
}
.particle {
animation: float-up linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.particle {
animation: none;
}
}
</style>