'use client'
import { useState, useEffect } from "react"
import { Autoplay, Navigation, Pagination } from "swiper/modules"
import { Swiper, SwiperSlide } from "swiper/react"
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
interface Review {
name: string;
rating: number;
text: string;
date: string;
profilePhoto: string | null;
reviewLink: string | null;
}
const swiperOptions = {
modules: [Autoplay, Pagination, Navigation],
slidesPerView: 3,
spaceBetween: 24,
autoplay: {
delay: 3500,
disableOnInteraction: false,
},
loop: true,
navigation: {
nextEl: '.h1n',
prevEl: '.h1p',
},
pagination: {
el: '.swiper-pagination',
clickable: true,
},
breakpoints: {
0: { slidesPerView: 1, spaceBetween: 16 },
640: { slidesPerView: 2, spaceBetween: 20 },
1024: { slidesPerView: 3, spaceBetween: 24 },
},
}
function StarRating({ rating }: { rating: number }) {
return (
{[1, 2, 3, 4, 5].map((i) => (
))}
);
}
function Avatar({ photo, name }: { photo: string | null; name: string }) {
const initials = name
.split(' ')
.map((n) => n[0])
.join('')
.substring(0, 2)
.toUpperCase();
const [imgError, setImgError] = useState(false);
if (photo && !imgError) {
return (
setImgError(true)}
style={{
width: '48px',
height: '48px',
borderRadius: '50%',
objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.15)',
flexShrink: 0,
}}
/>
);
}
// fallback: coloured circle with initials
const colors = ['#4285F4', '#34A853', '#EA4335', '#FBBC05', '#8E44AD', '#E67E22'];
const colorIndex = name.charCodeAt(0) % colors.length;
return (
{initials}
);
}
// Inline Google G SVG
function GoogleIcon() {
return (
);
}
export default function TestimonialSlider1() {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedIndex, setExpandedIndex] = useState(null);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
async function loadReviews() {
try {
const res = await fetch("/api/google-reviews/");
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.message || `HTTP ${res.status}`);
}
const data = await res.json();
// Only show 5-star reviews
const fiveStars = (data.reviews || []).filter((r: Review) => r.rating === 5);
setReviews(fiveStars);
} catch (err: any) {
console.error("Failed to fetch reviews:", err);
setError(err.message || "Could not load reviews.");
} finally {
setLoading(false);
}
}
loadReviews();
}, []);
// Swiper needs at least slidesPerView*2 slides for loop to work correctly
const loopedReviews =
reviews.length > 0 && reviews.length < 6
? [...reviews, ...reviews]
: reviews;
function truncate(text: string, max = 140) {
return text.length > max ? text.substring(0, max).trimEnd() + '…' : text;
}
if (!isClient) return null;
if (loading) {
return (
{[0, 1, 2].map((i) => (
))}
);
}
if (error || reviews.length === 0) {
return (
{error || "No reviews available at this time."}
);
}
return (
{loopedReviews.map((r, index) => {
const isExpanded = expandedIndex === index;
const hasMore = r.text.length > 140;
return (
{/* TOP: Avatar + Name + Google badge */}
{/* Review text */}
{r.text ? (
<>
{isExpanded ? r.text : truncate(r.text)}
{hasMore && (
)}
>
) : (
No written review.
)}
{/* BOTTOM: date + view link */}
{/*
*/}
);
})}
{/* Nav Buttons */}
);
}