✨ 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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user