Files
Portfolio-Game/frontend/app/pages/temoignages.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.7 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen">
<!-- Hero section -->
<section class="relative overflow-hidden bg-gradient-to-b from-sky-dark to-sky-darker py-16">
<div class="container mx-auto px-4">
<h1 class="text-center text-4xl font-narrative font-bold text-white md:text-5xl">
{{ $t('testimonials.page_title') }}
</h1>
<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-gray-400">
{{ $t('testimonials.page_description') }}
</p>
<!-- Toggle vue mode -->
<div class="mt-8 flex justify-center gap-2">
<button
type="button"
class="flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
:class="viewMode === 'dialogue' ? 'bg-sky-500 text-white' : 'bg-sky-dark/50 text-gray-400 hover:text-white'"
@click="viewMode = 'dialogue'"
>
<span>💬</span>
{{ $t('testimonials.dialogue_mode') }}
</button>
<button
type="button"
class="flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
:class="viewMode === 'list' ? 'bg-sky-500 text-white' : 'bg-sky-dark/50 text-gray-400 hover:text-white'"
@click="viewMode = 'list'"
>
<span>📋</span>
{{ $t('testimonials.list_mode') }}
</button>
</div>
</div>
<!-- Decorative elements -->
<div class="absolute -left-20 top-20 h-40 w-40 rounded-full bg-sky-500/5 blur-3xl" />
<div class="absolute -right-20 bottom-10 h-60 w-60 rounded-full bg-purple-500/5 blur-3xl" />
</section>
<!-- Testimonials content -->
<section class="container mx-auto px-4 py-12">
<!-- Loading state -->
<div v-if="status === 'pending'" class="flex items-center justify-center py-16">
<div class="h-8 w-8 animate-spin rounded-full border-4 border-sky-500 border-t-transparent" />
</div>
<!-- Error state -->
<div
v-else-if="error"
class="mx-auto max-w-md rounded-xl bg-red-500/10 p-8 text-center"
>
<span class="text-4xl">🕷</span>
<h2 class="mt-4 text-xl font-semibold text-red-400">
{{ $t('testimonials.load_error') }}
</h2>
<button
class="mt-4 rounded-lg bg-red-500/20 px-4 py-2 text-red-400 transition-colors hover:bg-red-500/30"
@click="refresh()"
>
{{ $t('common.retry') }}
</button>
</div>
<!-- Empty state -->
<div
v-else-if="!testimonials.length"
class="mx-auto max-w-md rounded-xl bg-sky-dark/50 p-8 text-center"
>
<span class="text-4xl">💬</span>
<h2 class="mt-4 text-xl font-semibold text-gray-300">
{{ $t('testimonials.no_testimonials') }}
</h2>
</div>
<!-- Content -->
<template v-else>
<!-- Mode Dialogue -->
<div v-if="viewMode === 'dialogue'" class="mx-auto max-w-3xl">
<DialoguePNJ
:testimonials="testimonials"
@complete="handleDialogueComplete"
/>
</div>
<!-- Mode Liste -->
<div v-else class="grid gap-6 md:grid-cols-2">
<TestimonialCard
v-for="testimonial in testimonials"
:key="testimonial.id"
:testimonial="testimonial"
class="testimonial-enter"
:style="{ animationDelay: `${testimonial.display_order * 100}ms` }"
/>
</div>
</template>
</section>
<!-- CTA section -->
<!-- Choice for next zone -->
<FeatureZoneEndChoice />
</div>
</template>
<script setup lang="ts">
import type { Testimonial } from '~/types/testimonial'
definePageMeta({
layout: 'adventure',
})
const { setPageMeta } = useSeo()
const { t } = useI18n()
const progressStore = useProgressionStore()
setPageMeta({
title: t('testimonials.page_title'),
description: t('testimonials.page_description'),
})
onMounted(() => {
progressStore.visitSection('testimonials')
})
const { data, status, error, refresh } = await useFetchTestimonials()
const testimonials = computed<Testimonial[]>(() => data.value?.data ?? [])
// Mode d'affichage (dialogue par défaut pour l'expérience immersive)
const viewMode = ref<'dialogue' | 'list'>('dialogue')
function handleDialogueComplete() {
// Action optionnelle à la fin du dialogue
}
</script>
<style scoped>
@media (prefers-reduced-motion: no-preference) {
.testimonial-enter {
animation: testimonial-fade-in 0.5s ease-out both;
}
@keyframes testimonial-fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>