feat: add SEO outreach templates, testimonial components, and supporting data schemas
This commit is contained in:
109
src/components/marketing/TestimonialsCarousel.tsx
Normal file
109
src/components/marketing/TestimonialsCarousel.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Star, CheckCircle, ChevronRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import type { Testimonial } from '@/lib/types';
|
||||
|
||||
interface Props {
|
||||
testimonials: Testimonial[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
function StarRating({ rating }: { rating: number }) {
|
||||
return (
|
||||
<div className="flex gap-0.5" aria-label={`${rating} out of 5 stars`}>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'fill-gray-200 text-gray-200'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
|
||||
return (
|
||||
<div className="w-[340px] sm:w-[380px] bg-white rounded-2xl shadow-sm border border-gray-100 p-6 flex flex-col h-full">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<StarRating rating={testimonial.rating} />
|
||||
{testimonial.verified && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs font-medium rounded-full border border-green-200">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">"{testimonial.title}"</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed flex-grow">{testimonial.content}</p>
|
||||
<div className="border-t border-gray-100 pt-4 mt-4">
|
||||
<span className="font-semibold text-gray-900 text-sm block">{testimonial.author.name}</span>
|
||||
{(testimonial.author.role || testimonial.author.company) && (
|
||||
<span className="text-xs text-gray-600 block">
|
||||
{[testimonial.author.role, testimonial.author.company].filter(Boolean).join(', ')}
|
||||
</span>
|
||||
)}
|
||||
{testimonial.author.location && (
|
||||
<span className="text-xs text-gray-400 block">{testimonial.author.location}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TestimonialsCarousel: React.FC<Props> = ({
|
||||
testimonials,
|
||||
title = 'What Our Customers Say',
|
||||
subtitle = 'Real experiences from businesses using QR Master',
|
||||
}) => {
|
||||
// Duplicate for seamless loop: when first set exits left, second set is identical → no visible reset
|
||||
const doubled = [...testimonials, ...testimonials];
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-gray-50 overflow-hidden">
|
||||
<style>{`
|
||||
@keyframes marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
.marquee-track {
|
||||
animation: marquee 90s linear infinite;
|
||||
}
|
||||
.marquee-track:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl mb-10">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full-width overflow mask */}
|
||||
<div className="w-full overflow-hidden">
|
||||
<div
|
||||
className="marquee-track flex gap-6"
|
||||
style={{ width: 'max-content' }}
|
||||
>
|
||||
{doubled.map((t, idx) => (
|
||||
<TestimonialCard key={`${t.id}-${idx}`} testimonial={t} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
href="/testimonials"
|
||||
className="inline-flex items-center text-blue-600 font-semibold hover:text-blue-700 transition-colors text-sm"
|
||||
>
|
||||
See all {testimonials.length} reviews
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user