Files
Portfolio-Game/frontend/app/pages/temoignages.vue
skycel 1cba01595b Add testimonials page with personality-styled cards (Story 2.6)
- Add testimonials table migration with personality enum
- Create Testimonial model with HasTranslations trait
- Add TestimonialSeeder with 4 test testimonials
- Create TestimonialController and TestimonialResource
- Add useFetchTestimonials composable
- Create TestimonialCard component with personality-based styling
- Add temoignages.vue page with loading/error states
- Add testimonials translations in FR/EN

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:59:54 +01:00

129 lines
3.8 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>
</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 grid -->
<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>
<!-- 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>
<!-- 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>
</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'
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 ?? [])
</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>