feat: add SEO outreach templates, testimonial components, and supporting data schemas
This commit is contained in:
@@ -14,14 +14,13 @@ import { Button } from '@/components/ui/Button';
|
||||
import { ReprintCalculatorTeaser } from '@/components/marketing/ReprintCalculatorTeaser';
|
||||
import { ScrollToTop } from '@/components/ui/ScrollToTop';
|
||||
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
|
||||
import { Testimonials } from '@/components/marketing/Testimonials';
|
||||
import { getFeaturedTestimonials } from '@/lib/testimonial-data';
|
||||
import { TestimonialsCarousel } from '@/components/marketing/TestimonialsCarousel';
|
||||
import { testimonials } from '@/lib/testimonial-data';
|
||||
import en from '@/i18n/en.json';
|
||||
|
||||
export default function HomePageClient() {
|
||||
// Always use English for marketing pages
|
||||
const t = en;
|
||||
const featuredTestimonials = getFeaturedTestimonials();
|
||||
|
||||
const industries = [
|
||||
'Restaurant Chain',
|
||||
@@ -44,8 +43,8 @@ export default function HomePageClient() {
|
||||
{/* Free Tools Grid */}
|
||||
<FreeToolsGrid />
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<Testimonials testimonials={featuredTestimonials} />
|
||||
{/* Testimonials Carousel */}
|
||||
<TestimonialsCarousel testimonials={testimonials} />
|
||||
|
||||
<React.Fragment>
|
||||
<StaticVsDynamic t={t} />
|
||||
|
||||
@@ -93,18 +93,23 @@ export const Testimonials: React.FC<TestimonialsProps> = ({
|
||||
<span className="font-semibold text-gray-900">
|
||||
{testimonial.author.name}
|
||||
</span>
|
||||
<div className="text-sm text-gray-600">
|
||||
{testimonial.author.company && (
|
||||
<span>{testimonial.author.company}</span>
|
||||
)}
|
||||
{testimonial.author.company && testimonial.author.location && (
|
||||
<span> • </span>
|
||||
)}
|
||||
{testimonial.author.location && (
|
||||
<span>{testimonial.author.location}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 mt-1">
|
||||
{(testimonial.author.role || testimonial.author.company) && (
|
||||
<div className="text-sm text-gray-700">
|
||||
{testimonial.author.role && (
|
||||
<span>{testimonial.author.role}</span>
|
||||
)}
|
||||
{testimonial.author.role && testimonial.author.company && (
|
||||
<span>, </span>
|
||||
)}
|
||||
{testimonial.author.company && (
|
||||
<span>{testimonial.author.company}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{testimonial.author.location && (
|
||||
<span className="text-sm text-gray-500">{testimonial.author.location}</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-400 mt-1">
|
||||
{testimonial.date}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
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