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>
113 lines
2.3 KiB
Vue
113 lines
2.3 KiB
Vue
<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>
|