Files
Portfolio-Game/frontend/app/components/feature/IntroSequence.vue
skycel 7e87a341a2 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>
2026-02-08 13:35:12 +01:00

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>