🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
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>
This commit is contained in:
493
docs/implementation-artifacts/4-7-revelation-monde-de-code.md
Normal file
493
docs/implementation-artifacts/4-7-revelation-monde-de-code.md
Normal file
@@ -0,0 +1,493 @@
|
||||
# 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
|
||||
|
||||
1. **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"
|
||||
2. **And** un paysage composé de blocs de code ASCII art s'affiche (montagnes, arbres, ville en code)
|
||||
3. **And** le code scroll/apparaît progressivement (animation)
|
||||
4. **And** l'avatar illustré de Célian est révélé au centre du monde de code
|
||||
5. **And** le narrateur (Le Bug) commente : "Tu l'as trouvé !"
|
||||
6. **And** le message "Tu m'as trouvé !" s'affiche avec effet de célébration
|
||||
7. **And** sur mobile, une version allégée mais émotionnellement équivalente s'affiche
|
||||
8. **And** `prefers-reduced-motion` affiche une version statique
|
||||
9. **And** une description alternative est disponible pour les screen readers
|
||||
10. **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
|
||||
|
||||
- [ ] **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)
|
||||
|
||||
- [ ] **Task 3: Créer l'ASCII art du paysage**
|
||||
- [ ] Montagnes en caractères (`/\`, `^`, etc.)
|
||||
- [ ] Arbres stylisés (`{}`, `[]`)
|
||||
- [ ] Soleil ou étoiles
|
||||
- [ ] Personnage au centre
|
||||
|
||||
- [ ] **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
|
||||
|
||||
```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
|
||||
|
||||
```vue
|
||||
<!-- 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 :**
|
||||
```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 :**
|
||||
```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 |
|
||||
|
||||
### File List
|
||||
|
||||
Reference in New Issue
Block a user