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>
14 KiB
14 KiB
Story 4.7: Révélation "Monde de Code"
Status: ready-for-dev
Story
As a visiteur ayant complété le parcours, I want vivre un moment waouh de révélation finale, so that la découverte du développeur est mémorable.
Acceptance Criteria
- Given le visiteur accède à la zone Contact (après challenge ou skip) When la révélation se déclenche Then une transition immersive mène vers le "Monde de Code"
- And un paysage composé de blocs de code ASCII art s'affiche (montagnes, arbres, ville en code)
- And le code scroll/apparaît progressivement (animation)
- And l'avatar illustré de Célian est révélé au centre du monde de code
- And le narrateur (Le Bug) commente : "Tu l'as trouvé !"
- And le message "Tu m'as trouvé !" s'affiche avec effet de célébration
- And sur mobile, une version allégée mais émotionnellement équivalente s'affiche
- And
prefers-reduced-motionaffiche une version statique - And une description alternative est disponible pour les screen readers
- And un bouton permet de continuer vers le formulaire de contact
Tasks / Subtasks
-
Task 1: Créer la page révélation (AC: #1, #10)
- Créer
frontend/app/pages/revelation.vue - Vérifier que le contact est débloqué
- Structure en phases : transition → monde de code → avatar → message
- Créer
-
Task 2: Créer le composant CodeWorld (AC: #2, #3)
- Créer
frontend/app/components/feature/CodeWorld.vue - ASCII art représentant un paysage (montagnes, arbres, soleil)
- Animation de révélation ligne par ligne
- Couleurs syntaxiques (comme du code)
- Créer
-
Task 3: Créer l'ASCII art du paysage
- Montagnes en caractères (
/\,^, etc.) - Arbres stylisés (
{},[]) - Soleil ou étoiles
- Personnage au centre
- Montagnes en caractères (
-
Task 4: Révéler l'avatar de Célian (AC: #4)
- Image illustrée de Célian
- Animation d'apparition (fade + scale)
- Position centrale sur le monde de code
-
Task 5: Message du narrateur (AC: #5)
- Le Bug s'exclame "Tu l'as trouvé !"
- Utiliser NarratorBubble ou message intégré
- Ton enthousiaste et célébratoire
-
Task 6: Message de Célian (AC: #6)
- "Tu m'as trouvé !" avec effet typewriter
- Animation de célébration autour
- Signature de Célian
-
Task 7: Version mobile (AC: #7)
- ASCII art simplifié ou image de remplacement
- Mêmes éléments clés : avatar, message, émotion
- Performance optimisée
-
Task 8: Accessibilité (AC: #8, #9)
- Respecter prefers-reduced-motion (version statique)
- Description alternative pour screen readers
- aria-label descriptif
-
Task 9: Tests et validation
- Tester l'animation complète
- Vérifier la version mobile
- Tester prefers-reduced-motion
- Valider l'accessibilité
Dev Notes
ASCII Art du Monde de Code
* . *
* . . *
. ___ .
* . / \ *
. / ^ \ . *
* / /^\ \ *
. /____/ \____\ .
* | | | | *
. | | | | .
_______| |_____| |_______
/ | | | | \
{ Vue }| TS |{PHP}| DB |{Nuxt}
\_______________________/
|| || ||
{ } { } { }
|| || ||
___||_____||_____||___
| YOU |
| FOUND ME! 🎉 |
|_____________________|
Page revelation.vue
<!-- frontend/app/pages/revelation.vue -->
<script setup lang="ts">
const { t } = useI18n()
const router = useRouter()
const progressionStore = useProgressionStore()
const narrator = useNarrator()
const reducedMotion = useReducedMotion()
// Vérifier que le contact est débloqué
if (!progressionStore.contactUnlocked) {
navigateTo('/')
}
// Phases de la révélation
type Phase = 'transition' | 'codeworld' | 'avatar' | 'message' | 'complete'
const currentPhase = ref<Phase>('transition')
// Progression des phases
async function advancePhase() {
const phases: Phase[] = ['transition', 'codeworld', 'avatar', 'message', 'complete']
const currentIndex = phases.indexOf(currentPhase.value)
if (currentIndex < phases.length - 1) {
currentPhase.value = phases[currentIndex + 1]
// Actions spécifiques par phase
if (currentPhase.value === 'avatar') {
await narrator.showMessage('revelation_found')
}
}
}
// Démarrer la séquence
onMounted(() => {
if (reducedMotion.value) {
// Version statique : aller directement à complete
currentPhase.value = 'complete'
} else {
// Animation : transition vers codeworld après 1.5s
setTimeout(() => {
advancePhase()
}, 1500)
}
})
function goToContact() {
router.push('/contact')
}
</script>
<template>
<div class="revelation-page min-h-screen bg-sky-dark overflow-hidden">
<!-- Screen reader description -->
<p class="sr-only">
{{ t('revelation.srDescription') }}
</p>
<!-- Phase : Transition -->
<Transition name="fade">
<div
v-if="currentPhase === 'transition'"
class="fixed inset-0 flex items-center justify-center bg-black z-50"
>
<p class="font-narrative text-2xl text-sky-text animate-pulse">
{{ t('revelation.transition') }}
</p>
</div>
</Transition>
<!-- Phase : Code World -->
<div
v-show="currentPhase !== 'transition'"
class="relative min-h-screen flex flex-col items-center justify-center p-4"
>
<!-- ASCII Code World -->
<CodeWorld
:animate="currentPhase === 'codeworld'"
@complete="advancePhase"
class="mb-8"
/>
<!-- Avatar de Célian -->
<Transition name="scale-fade">
<div
v-if="['avatar', 'message', 'complete'].includes(currentPhase)"
class="relative"
>
<img
src="/images/avatar-celian.svg"
alt="Célian"
class="w-32 h-32 md:w-48 md:h-48 rounded-full border-4 border-sky-accent shadow-2xl shadow-sky-accent/30"
/>
<!-- Sparkles autour -->
<div class="absolute inset-0 -m-4">
<span
v-for="i in 8"
:key="i"
class="absolute text-xl animate-pulse"
:style="{
top: `${50 + 45 * Math.sin(i * Math.PI / 4)}%`,
left: `${50 + 45 * Math.cos(i * Math.PI / 4)}%`,
transform: 'translate(-50%, -50%)',
animationDelay: `${i * 100}ms`,
}"
>
✨
</span>
</div>
</div>
</Transition>
<!-- Message "Tu m'as trouvé !" -->
<Transition name="slide-up">
<div
v-if="['message', 'complete'].includes(currentPhase)"
class="mt-8 text-center"
>
<h1 class="text-4xl md:text-5xl font-ui font-bold text-sky-accent mb-4">
{{ t('revelation.foundMe') }}
</h1>
<p class="font-narrative text-xl text-sky-text mb-2">
{{ t('revelation.greeting') }}
</p>
<p class="font-ui text-sky-text-muted">
— Célian, {{ t('revelation.title') }}
</p>
</div>
</Transition>
<!-- Bouton continuer -->
<Transition name="fade">
<button
v-if="currentPhase === 'complete'"
type="button"
class="mt-12 px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-xl hover:bg-sky-accent/90 transition-colors shadow-lg shadow-sky-accent/30"
@click="goToContact"
>
{{ t('revelation.contactMe') }}
</button>
</Transition>
</div>
<!-- Version reduced-motion -->
<div
v-if="reducedMotion && currentPhase === 'complete'"
class="fixed inset-0 flex flex-col items-center justify-center p-8 bg-sky-dark"
>
<img
src="/images/avatar-celian.svg"
alt="Célian"
class="w-32 h-32 rounded-full border-4 border-sky-accent mb-8"
/>
<h1 class="text-3xl font-ui font-bold text-sky-accent mb-4">
{{ t('revelation.foundMe') }}
</h1>
<p class="font-narrative text-lg text-sky-text text-center mb-8">
{{ t('revelation.greeting') }}
</p>
<button
type="button"
class="px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-xl"
@click="goToContact"
>
{{ t('revelation.contactMe') }}
</button>
</div>
</div>
</template>
<style scoped>
.scale-fade-enter-active,
.scale-fade-leave-active {
transition: all 0.8s ease;
}
.scale-fade-enter-from {
opacity: 0;
transform: scale(0.5);
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.6s ease;
}
.slide-up-enter-from {
opacity: 0;
transform: translateY(30px);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
Composant CodeWorld
<!-- frontend/app/components/feature/CodeWorld.vue -->
<script setup lang="ts">
const props = defineProps<{
animate: boolean
}>()
const emit = defineEmits<{
complete: []
}>()
const reducedMotion = useReducedMotion()
// ASCII Art du monde de code
const asciiArt = `
* . * . *
* . . * .
. ___ .
. / \\ *
* / ^ \\ . *
/ /^\\ \\ *
/____/ \\____\\ .
* | | | | *
| | | | .
____| |_____| |_______
| | | |
{Vue}| TS |{PHP}| DB |{Nuxt}
____________________________
|| || ||
{ } { } { }
|| || ||
`.trim()
const lines = asciiArt.split('\n')
const visibleLines = ref(reducedMotion.value ? lines.length : 0)
// Animation ligne par ligne
watch(() => props.animate, (shouldAnimate) => {
if (shouldAnimate && !reducedMotion.value) {
animateLines()
}
})
function animateLines() {
const interval = setInterval(() => {
if (visibleLines.value < lines.length) {
visibleLines.value++
} else {
clearInterval(interval)
setTimeout(() => {
emit('complete')
}, 500)
}
}, 100)
}
// Coloration syntaxique simple
function colorize(line: string): string {
return line
.replace(/{(\w+)}/g, '<span class="text-green-400">{$1}</span>')
.replace(/\|/g, '<span class="text-sky-accent">|</span>')
.replace(/\*/g, '<span class="text-yellow-400">*</span>')
.replace(/\./g, '<span class="text-blue-400">.</span>')
}
</script>
<template>
<div
class="code-world font-mono text-xs md:text-sm text-sky-text-muted leading-tight"
role="img"
:aria-label="$t('revelation.codeWorldAlt')"
>
<pre class="overflow-hidden"><code><template v-for="(line, index) in lines" :key="index"><span
v-if="index < visibleLines"
v-html="colorize(line)"
class="block"
></span></template></code></pre>
</div>
</template>
<style scoped>
.code-world {
text-shadow: 0 0 10px rgba(250, 120, 79, 0.3);
}
</style>
Clés i18n
fr.json :
{
"revelation": {
"transition": "Le voilà...",
"foundMe": "Tu m'as trouvé !",
"greeting": "Bienvenue dans mon monde de code. Je suis Célian, le développeur que tu cherchais depuis le début.",
"title": "Développeur Web Fullstack",
"contactMe": "Me contacter",
"codeWorldAlt": "Un paysage stylisé composé de caractères de code, représentant l'univers du développeur",
"srDescription": "Vous avez découvert le développeur ! Célian vous accueille dans son monde de code."
}
}
en.json :
{
"revelation": {
"transition": "There he is...",
"foundMe": "You found me!",
"greeting": "Welcome to my world of code. I'm Célian, the developer you've been looking for all along.",
"title": "Fullstack Web Developer",
"contactMe": "Contact me",
"codeWorldAlt": "A stylized landscape made of code characters, representing the developer's universe",
"srDescription": "You discovered the developer! Célian welcomes you to his world of code."
}
}
Dépendances
Cette story nécessite :
- Story 3.5 : Store de progression (contactUnlocked)
- Story 3.2 : useReducedMotion
- Story 3.3 : useNarrator (révélation)
Cette story prépare pour :
- Story 4.8 : Page contact (destination finale)
Project Structure Notes
Fichiers à créer :
frontend/app/
├── pages/
│ └── revelation.vue # CRÉER
├── components/feature/
│ └── CodeWorld.vue # CRÉER
└── public/images/
└── avatar-celian.svg # CRÉER (asset)
Fichiers à modifier :
frontend/i18n/fr.json # AJOUTER revelation.*
frontend/i18n/en.json # AJOUTER revelation.*
References
- [Source: docs/planning-artifacts/epics.md#Story-4.7]
- [Source: docs/planning-artifacts/ux-design-specification.md#Revelation]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Revelation]
Technical Requirements
| Requirement | Value | Source |
|---|---|---|
| ASCII Art | Paysage stylisé | Epics |
| Avatar | Image de Célian | Epics |
| Message | "Tu m'as trouvé !" | Epics |
| Accessibilité | prefers-reduced-motion, aria | 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 |