managarten/packages/shared-landing-ui/src/sections/TestimonialSection.astro
Wuesteon 4cc1ad2c92 🔧 chore: fix turbo infinite recursion and update dependencies
- Remove recursive `turbo run type-check` from parent packages (chat, zitare, voxel-lava)
- Increase turbo concurrency from 2 to 5
- Add documentation for turbo anti-pattern in CLAUDE.md
- Skip type-check temporarily for apps with pending migrations
- Update picture mobile stores to use camelCase API response properties
- Add shared-nestjs-auth dependency to chat and picture backends
- Clean up unused design-token files from picture package
- Update shared-landing-ui components and feedback service config
2025-12-02 02:43:47 +01:00

121 lines
3.9 KiB
Text

---
/**
* Shared Testimonial Section component
* Displays customer testimonials in a grid
*/
import Container from '../atoms/Container.astro';
import Card from '../atoms/Card.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
interface Testimonial {
name: string;
role?: string;
company?: string;
text: string;
image?: string;
rating?: number;
}
interface Props {
title: string;
subtitle?: string;
testimonials: Testimonial[];
columns?: 1 | 2 | 3;
showRating?: boolean;
class?: string;
id?: string;
}
const {
title,
subtitle,
testimonials,
columns = 3,
showRating = true,
class: className = '',
id
} = Astro.props;
const gridCols = {
1: 'max-w-2xl mx-auto',
2: 'md:grid-cols-2 max-w-4xl mx-auto',
3: 'md:grid-cols-2 lg:grid-cols-3'
};
---
<section id={id} class:list={["py-16 md:py-24", className]}>
<Container>
<SectionHeader title={title} subtitle={subtitle} />
<div class:list={["grid gap-6 md:gap-8", gridCols[columns]]}>
{testimonials.map((testimonial) => (
<Card variant="bordered" padding="lg">
<div class="flex flex-col h-full">
<!-- Header with avatar and info -->
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0">
{testimonial.image ? (
<img
src={testimonial.image}
alt={testimonial.name}
class="w-12 h-12 rounded-full object-cover border-2 border-[var(--color-primary)]/20"
width="48"
height="48"
loading="lazy"
decoding="async"
/>
) : (
<div class="w-12 h-12 rounded-full bg-[var(--color-primary)]/20 flex items-center justify-center">
<span class="text-xl font-semibold text-[var(--color-primary)]">
{testimonial.name.charAt(0)}
</span>
</div>
)}
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-[var(--color-text-primary)] truncate">
{testimonial.name}
</h3>
{testimonial.role && (
<p class="text-sm text-[var(--color-text-secondary)] truncate">
{testimonial.role}
</p>
)}
{testimonial.company && (
<p class="text-sm text-[var(--color-text-muted)] truncate">
{testimonial.company}
</p>
)}
</div>
</div>
<!-- Quote -->
<blockquote class="flex-1 text-[var(--color-text-secondary)] italic mb-4 leading-relaxed">
"{testimonial.text}"
</blockquote>
<!-- Rating -->
{showRating && testimonial.rating && (
<div class="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<svg
class:list={[
"w-5 h-5",
i < (testimonial.rating ?? 0) ? "text-[var(--color-primary)]" : "text-[var(--color-border)]"
]}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
)}
</div>
</Card>
))}
</div>
<slot />
</Container>
</section>