✨ 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:
112
frontend/app/components/feature/IntroSequence.vue
Normal file
112
frontend/app/components/feature/IntroSequence.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div
|
||||
class="intro-sequence cursor-pointer"
|
||||
tabindex="0"
|
||||
@click="handleInteraction"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<!-- Avatar du Bug -->
|
||||
<div class="mb-8">
|
||||
<img
|
||||
src="/images/bug/bug-stage-1.svg"
|
||||
alt="Le Bug"
|
||||
class="w-32 h-32 mx-auto"
|
||||
:class="{ 'animate-float': !reducedMotion }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Texte avec typewriter -->
|
||||
<div class="bg-sky-dark-50/80 backdrop-blur rounded-xl p-8 border border-sky-dark-100">
|
||||
<p class="font-narrative text-xl md:text-2xl text-sky-text leading-relaxed min-h-[4rem]">
|
||||
{{ displayedText }}
|
||||
<span
|
||||
v-if="isTyping"
|
||||
class="inline-block w-0.5 h-6 bg-sky-accent ml-1"
|
||||
:class="{ 'animate-blink': !reducedMotion }"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<!-- Indication pour skip -->
|
||||
<p
|
||||
v-if="isTyping"
|
||||
class="text-sm text-sky-text/50 mt-4 font-ui"
|
||||
>
|
||||
{{ $t('narrator.clickToSkip') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: []
|
||||
skip: []
|
||||
}>()
|
||||
|
||||
const reducedMotion = useReducedMotion()
|
||||
const textRef = computed(() => props.text)
|
||||
const { displayedText, isTyping, skip, start } = useTypewriter(textRef, { speed: 35 })
|
||||
|
||||
// Démarrer le typewriter quand le texte change
|
||||
watch(() => props.text, (newText) => {
|
||||
if (newText) {
|
||||
start()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Watcher pour détecter quand le texte est complet
|
||||
watch(isTyping, (typing) => {
|
||||
if (!typing && displayedText.value === props.text) {
|
||||
emit('complete')
|
||||
}
|
||||
})
|
||||
|
||||
function handleInteraction() {
|
||||
if (isTyping.value) {
|
||||
skip()
|
||||
emit('skip')
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.code === 'Space' || e.code === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleInteraction()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.animate-blink {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
.intro-sequence:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-float,
|
||||
.animate-blink {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user