Files
Portfolio-Game/frontend/app/pages/temoignages.vue
skycel 99fa61fcaa feat(frontend): système narrateur contextuel avec arc de révélation
Story 3.3 : Textes narrateur contextuels et arc de révélation
- Composable useNarrator.ts avec queue de messages prioritaires
- Composable useIdleDetection.ts (détection inactivité 30s)
- Plugin narrator-transitions.client.ts (déclencheurs de navigation)
- Layout adventure.vue avec NarratorBubble intégré
- Store progression: narratorStage devient un getter calculé (0-20-40-60-80%)
- Pages projets, competences, temoignages, parcours utilisent layout adventure
- Messages: intro, transitions, encouragements 25/50/75%, hints, contact_unlocked

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 03:04:07 +01:00

169 lines
5.3 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 -->
<section class="container mx-auto px-4 pb-16">
<div class="rounded-xl bg-gradient-to-r from-sky-500/10 to-purple-500/10 p-8 text-center">
<h2 class="text-2xl font-semibold text-white">
{{ $t('testimonials.cta_title') }}
</h2>
<p class="mt-2 text-gray-400">
{{ $t('testimonials.cta_description') }}
</p>
<NuxtLink
to="/contact"
class="mt-4 inline-block rounded-lg bg-sky-500 px-6 py-3 font-medium text-white transition-colors hover:bg-sky-400"
>
{{ $t('testimonials.cta_button') }}
</NuxtLink>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import type { Testimonial } from '~/types/testimonial'
definePageMeta({
layout: 'adventure',
})
const { setPageMeta } = useSeo()
const { t } = useI18n()
const progressStore = useProgressStore()
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>