Add DialoguePNJ component with typewriter effect (Story 2.7)

- Create useReducedMotion composable for motion preferences
- Create useTypewriter composable with accelerate/skip support
- Add DialoguePNJ component with Zelda-style dialogue system
- Add personality-based styling (sage, sarcastique, enthousiaste, professionnel)
- Implement keyboard navigation (arrows, space)
- Add toggle between dialogue and list view modes
- Add i18n translations for dialogue UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 11:07:40 +01:00
parent 1cba01595b
commit cfc9cca34f
8 changed files with 522 additions and 60 deletions

View File

@@ -9,6 +9,28 @@
<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 -->
@@ -16,15 +38,11 @@
<div class="absolute -right-20 bottom-10 h-60 w-60 rounded-full bg-purple-500/5 blur-3xl" />
</section>
<!-- Testimonials grid -->
<!-- Testimonials content -->
<section class="container mx-auto px-4 py-12">
<!-- Loading state -->
<div v-if="status === 'pending'" class="grid gap-6 md:grid-cols-2">
<div
v-for="i in 4"
:key="i"
class="h-64 animate-pulse rounded-xl bg-sky-dark/50"
/>
<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 -->
@@ -55,16 +73,27 @@
</h2>
</div>
<!-- Testimonials -->
<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>
<!-- 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 -->
@@ -106,6 +135,13 @@ onMounted(() => {
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>