Files
Portfolio-Game/frontend/app/pages/intro.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

155 lines
4.2 KiB
Vue

<template>
<div class="intro-page min-h-screen bg-sky-dark relative overflow-hidden">
<!-- Fond d'ambiance -->
<FeatureIntroBackground />
<!-- Contenu principal -->
<div class="relative z-10 flex flex-col items-center justify-center min-h-screen p-8">
<!-- Séquence narrative -->
<Transition name="fade" mode="out-in">
<div
v-if="!isChoiceStep"
:key="currentStep"
class="max-w-2xl mx-auto text-center"
>
<FeatureIntroSequence
:text="currentText"
@complete="handleTextComplete"
@skip="handleTextComplete"
/>
<!-- Bouton continuer -->
<Transition name="fade">
<button
v-if="isTextComplete"
type="button"
class="mt-8 px-8 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg hover:opacity-90 transition-opacity focus-visible:outline-2 focus-visible:outline-sky-accent focus-visible:outline-offset-2"
@click="nextStep"
>
{{ isLastTextStep ? $t('intro.startExploring') : $t('intro.continue') }}
</button>
</Transition>
</div>
<!-- Choix après l'intro -->
<div v-else key="choice" class="w-full max-w-3xl mx-auto">
<FeatureChoiceCards :choice-point="introChoicePoint" />
</div>
</Transition>
<!-- Bouton skip (toujours visible sauf sur le choix) -->
<button
v-if="!isChoiceStep"
type="button"
class="absolute bottom-8 right-8 text-sky-text/50 hover:text-sky-text text-sm font-ui underline transition-colors focus-visible:outline-2 focus-visible:outline-sky-accent"
@click="skipIntro"
>
{{ $t('intro.skip') }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { CHOICE_POINTS } from '~/types/choice'
import type { NarratorContext } from '~/composables/useFetchNarratorText'
definePageMeta({
layout: 'adventure',
})
const { t } = useI18n()
const localePath = useLocalePath()
const progressionStore = useProgressionStore()
const { fetchText } = useFetchNarratorText()
// Rediriger si pas de héros sélectionné
onMounted(() => {
if (!progressionStore.hero) {
navigateTo(localePath('/'))
}
})
// Étapes de la séquence
const steps: (NarratorContext | 'choice')[] = ['intro_sequence_1', 'intro_sequence_2', 'intro_sequence_3', 'choice']
const currentStepIndex = ref(0)
const currentStep = computed(() => steps[currentStepIndex.value])
const isLastTextStep = computed(() => currentStepIndex.value === steps.length - 2)
const isChoiceStep = computed(() => currentStep.value === 'choice')
// Texte actuel
const currentText = ref('')
const isTextComplete = ref(false)
// Point de choix pour l'intro
const introChoicePoint = CHOICE_POINTS.intro_first_choice
// Fallback texts
const fallbackTexts: Record<string, string> = {
intro_sequence_1: t('intro.fallback.seq1'),
intro_sequence_2: t('intro.fallback.seq2'),
intro_sequence_3: t('intro.fallback.seq3'),
}
async function loadCurrentText() {
if (isChoiceStep.value) return
const context = currentStep.value as NarratorContext
const response = await fetchText(context, progressionStore.hero || undefined)
if (response?.text) {
currentText.value = response.text
} else {
// Fallback si l'API n'est pas disponible
currentText.value = fallbackTexts[context] || ''
}
}
function handleTextComplete() {
isTextComplete.value = true
}
function nextStep() {
if (currentStepIndex.value < steps.length - 1) {
currentStepIndex.value++
isTextComplete.value = false
loadCurrentText()
}
}
function skipIntro() {
currentStepIndex.value = steps.length - 1 // Aller directement au choix
progressionStore.setIntroSeen(true)
}
// Charger le premier texte
onMounted(() => {
loadCurrentText()
})
// Marquer l'intro comme vue quand on quitte
onBeforeUnmount(() => {
progressionStore.setIntroSeen(true)
})
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
.fade-enter-active,
.fade-leave-active {
transition: none;
}
}
</style>