"use client" import Image from "next/image" import { useState, useEffect } from "react" import { ChevronLeft, ChevronRight, Star } from "lucide-react" import { motion, AnimatePresence } from "framer-motion" import { ensureHttpsImageUrl } from "@/lib/utils" interface Testimonial { id: number | string name: string title: string organization: string image: string text: string rating: number } export default function Testimonials() { const [testimonials, setTestimonials] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [startIndex, setStartIndex] = useState(0) const itemsPerPage = 3 // Fetch testimonials from API useEffect(() => { const fetchTestimonials = async () => { try { // Use Next.js API route instead of direct Strapi call const res = await fetch("/api/testimonials", { headers: { Accept: "application/json" }, }) if (!res.ok) throw new Error(`Testimonials API status ${res.status}`) const json = await res.json() const items = Array.isArray(json?.data) ? json.data : [] const mapped = items .filter((item: any) => { // Only include items that have an avatar image const attrs = item?.attributes ?? item ?? {} const avatarUrl = attrs?.avatar?.data?.attributes?.url || attrs?.avatar?.data?.url || attrs?.avatar?.url || attrs?.avatar || "" return !!avatarUrl }) .map((item: any, index: number): Testimonial => { const attrs = item?.attributes ?? item ?? {} // Normalize rating 0-5 const rawRating = Number(attrs?.rating ?? 0) const rating = Number.isFinite(rawRating) ? Math.min(5, Math.max(0, Math.round(rawRating))) : 0 // Resolve media URL - handle both nested and direct formats const avatarData = attrs?.avatar?.data || attrs?.avatar const avatarAttrs = avatarData?.attributes || avatarData || {} const avatarUrl = avatarAttrs?.url || attrs?.avatar?.url || "" // Ensure HTTPS to prevent Mixed Content errors const image = ensureHttpsImageUrl(avatarUrl) return { id: item?.id ?? attrs?.id ?? index, name: attrs?.name ?? "", title: attrs?.role ?? "", organization: attrs?.company ?? "", text: attrs?.quote ?? "", rating, image, } }) setTestimonials(mapped) setError(null) } catch (err: any) { console.error("Failed to load testimonials:", err) setTestimonials([]) setError("Failed to load testimonials") } finally { setIsLoading(false) } } fetchTestimonials() }, []) const nextSlide = () => { if (testimonials.length === 0) return setStartIndex((prev) => (prev + 1) % Math.max(1, testimonials.length)) } const prevSlide = () => { if (testimonials.length === 0) return setStartIndex((prev) => (prev === 0 ? testimonials.length - 1 : prev - 1)) } // Sliding window over testimonials const visibleTestimonials: Testimonial[] = [] const visibleCount = Math.min(itemsPerPage, testimonials.length) for (let i = 0; i < visibleCount; i++) { const index = (startIndex + i) % Math.max(1, testimonials.length || 1) const item = testimonials[index] if (item) visibleTestimonials.push(item) } const handleNext = () => { setStartIndex((prev) => (prev + 1) % testimonials.length) } const handlePrev = () => { setStartIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length) } return (

Testimonials

{/* Navigation Buttons */} {/* Carousel Viewport */}
{isLoading && (
Loading testimonials...
)} {!isLoading && testimonials.length === 0 && (
No testimonials available at the moment.
)}
{visibleTestimonials.map((testimonial, index) => ( 0 ? 'hidden lg:block' : ''} ${index > 1 ? 'xl:block hidden lg:hidden' : ''}`} > {/* Card Design: - White background - Padding around - Inner border that changes color on hover */}
{/* Profile Image */} {testimonial.image && (
{testimonial.name}
)} {/* Star Rating */}
{Array.from({ length: 5 }).map((_, i) => { const isFilled = i < testimonial.rating return ( ) })}
{/* Text - Serif font as per design image */}

{testimonial.text}

{/* Divider */}
{/* Author Info */}

{testimonial.name}

{testimonial.title}

{testimonial.organization}

))}
) }