---
/**
* TestimonialCards: quotes as an even row of cards, each with a star rating
* (optional) and an initials avatar.
*
* @example
* <TestimonialCards title="Heard on" accent="the trail." items={[{ quote: "...", name: "Maya Ortiz", rating: 5 }]} />
*/
interface Item {
quote: string;
name: string;
role?: string;
/** 1 to 5. Omit to hide the stars. */
rating?: number;
}
interface Props {
title?: string;
accent?: string;
items: Item[];
as?: "h2" | "h3";
class?: string;
}
const { title, accent, items, as: Heading = "h2", class: className } = Astro.props;
const initials = (name: string) =>
name
.split(/\s+/)
.map((p) => p[0])
.slice(0, 2)
.join("")
.toUpperCase();
const star = "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";
---
<section class:list={["vs-testimonial-cards", className]}>
<div class="vs-testimonial-cards__inner">
{title && <Heading class="vs-display vs-testimonial-cards__title">{title}{accent && <> <em>{accent}</em></>}</Heading>}
<ul class="vs-testimonial-cards__grid">
{
items.map((q) => (
<li>
<figure class="vs-testimonial-cards__card">
{q.rating && (
<p class="vs-testimonial-cards__stars" role="img" aria-label={`${q.rating} out of 5 stars`}>
{Array.from({ length: 5 }, (_, i) => (
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" class:list={{ "is-off": i >= q.rating! }}>
<path d={star} />
</svg>
))}
</p>
)}
<blockquote>
<p>{q.quote}</p>
</blockquote>
<figcaption class="vs-testimonial-cards__by">
<span class="vs-testimonial-cards__avatar" aria-hidden="true">{initials(q.name)}</span>
<span>
<span class="vs-testimonial-cards__name">{q.name}</span>
{q.role && <span class="vs-testimonial-cards__role">{q.role}</span>}
</span>
</figcaption>
</figure>
</li>
))
}
</ul>
</div>
</section>
<style>
.vs-testimonial-cards {
container-type: inline-size;
background: var(--vs-page);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
}
.vs-testimonial-cards__inner {
max-width: var(--vs-container);
margin-inline: auto;
padding: var(--vs-section-y) var(--vs-gutter);
}
.vs-testimonial-cards__title {
margin-bottom: clamp(28px, 5cqi, 56px);
font-size: clamp(2rem, 6.5cqi, 4.25rem);
}
.vs-testimonial-cards__grid {
display: grid;
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
@container (min-width: 640px) {
.vs-testimonial-cards__grid {
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
}
.vs-testimonial-cards figure,
.vs-testimonial-cards blockquote,
.vs-testimonial-cards p {
margin: 0;
}
.vs-testimonial-cards__card {
display: flex;
height: 100%;
flex-direction: column;
gap: 18px;
padding: 28px;
border: var(--vs-border-w) solid var(--vs-line);
border-radius: var(--vs-radius-card);
background: var(--vs-surface);
box-shadow: var(--vs-shadow-sm);
}
.vs-testimonial-cards__card blockquote {
flex: 1;
font-size: 1.0625rem;
line-height: 1.5;
}
.vs-testimonial-cards__stars {
display: flex;
gap: 2px;
color: var(--vs-accent);
}
.vs-testimonial-cards__stars svg {
fill: currentColor;
}
.vs-testimonial-cards__stars .is-off {
fill: var(--vs-line-strong);
}
.vs-testimonial-cards__by {
display: flex;
align-items: center;
gap: 12px;
padding-top: 18px;
border-top: 1px solid var(--vs-line);
font-size: 0.875rem;
}
.vs-testimonial-cards__by > span:last-child {
display: flex;
flex-direction: column;
}
.vs-testimonial-cards__avatar {
display: inline-flex;
width: 38px;
height: 38px;
flex: none;
align-items: center;
justify-content: center;
border-radius: var(--vs-radius-pill);
background: var(--vs-ink);
color: var(--vs-ink-inverse);
font-size: 0.75rem;
font-weight: 600;
}
.vs-testimonial-cards__name {
font-weight: 600;
}
.vs-testimonial-cards__role {
color: var(--vs-ink-3);
}
</style>