🎉 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:
2026-02-05 02:08:56 +01:00
commit ec1ae92799
116 changed files with 55669 additions and 0 deletions

View 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