TestimonialsMasonry Social Proof
Masonry testimonial grid — variabele hoogtes, featured card in accent kleur.
src/components/social-proof/TestimonialsMasonry.astro
---
/**
* TestimonialsMasonry
* Masonry-stijl testimonial grid — variabele hoogtes, social-proof cards.
*/
interface Testimonial {
text: string;
author: string;
role?: string;
company?: string;
avatar?: string;
rating?: number;
featured?: boolean;
}
interface Props {
eyebrow?: string;
headline?: string;
testimonials: Testimonial[];
}
const { eyebrow, headline, testimonials = [] } = Astro.props;
---
<section class="tm" data-component="testimonials-masonry">
<div class="tm__inner">
{(eyebrow || headline) && (
<div class="tm__header">
{eyebrow && <p class="tm__eyebrow">{eyebrow}</p>}
{headline && <h2 class="tm__title" set:html={headline} />}
</div>
)}
<div class="tm__masonry">
{testimonials.map(t => (
<div class:list={['tm__card', { 'tm__card--featured': t.featured }]}>
{t.rating && (
<div class="tm__stars">
{Array.from({ length: t.rating }).map(() => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
))}
</div>
)}
<p class="tm__text">{t.text}</p>
<div class="tm__author">
{t.avatar && <img src={t.avatar} alt={t.author} class="tm__avatar" />}
<div>
<div class="tm__author-name">{t.author}</div>
{(t.role || t.company) && (
<div class="tm__author-meta">
{t.role}{t.role && t.company ? ', ' : ''}{t.company}
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
</section>
<style>
.tm {
background: #f5f5f7;
padding: 5rem 1.5rem;
}
.tm__inner {
max-width: 1200px;
margin: 0 auto;
}
.tm__header {
text-align: center;
margin-bottom: 3rem;
}
.tm__eyebrow {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent, #6366f1);
margin-bottom: 0.75rem;
}
.tm__title {
font-size: clamp(1.875rem, 3vw, 2.75rem);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-primary, #0a0a0a);
}
.tm__title :global(em) {
font-style: normal;
color: var(--color-accent, #6366f1);
}
/* Masonry */
.tm__masonry {
columns: 3;
column-gap: 1.25rem;
}
.tm__card {
break-inside: avoid;
background: #fff;
border-radius: calc(var(--radius, 0.5rem) * 1.5);
padding: 1.5rem;
margin-bottom: 1.25rem;
border: 1px solid rgba(0,0,0,0.06);
transition: box-shadow 0.2s, transform 0.2s;
}
.tm__card:hover {
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
transform: translateY(-2px);
}
.tm__card--featured {
background: var(--color-accent, #6366f1);
color: #fff;
border-color: transparent;
}
.tm__stars {
display: flex;
gap: 2px;
margin-bottom: 0.875rem;
color: #f59e0b;
}
.tm__card--featured .tm__stars { color: rgba(255,255,255,0.9); }
.tm__text {
font-size: 0.9375rem;
line-height: 1.65;
color: var(--color-primary, #0a0a0a);
margin-bottom: 1.25rem;
}
.tm__card--featured .tm__text { color: rgba(255,255,255,0.9); }
.tm__author {
display: flex;
align-items: center;
gap: 0.625rem;
}
.tm__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.tm__author-name {
font-size: 0.875rem;
font-weight: 700;
color: var(--color-primary, #0a0a0a);
}
.tm__card--featured .tm__author-name { color: #fff; }
.tm__author-meta {
font-size: 0.8125rem;
color: var(--color-muted, #6b7280);
}
.tm__card--featured .tm__author-meta { color: rgba(255,255,255,0.65); }
@media (max-width: 900px) {
.tm__masonry { columns: 2; }
}
@media (max-width: 540px) {
.tm__masonry { columns: 1; }
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
testimonials * | Testimonial[] | — | Array van testimonials. featured: true voor accent kaart. |
eyebrow | string | — | Label boven sectie |
headline | string | — | Sectie headline |
* = verplicht