SFS/components/testimonials.tsx
2025-12-16 10:03:26 +05:30

237 lines
9.1 KiB
TypeScript

"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<Testimonial[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(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 (
<section className="w-full bg-[#EAEAEA] py-16 px-4 md:py-24">
<div className="max-w-[1100px] mx-auto">
<h2 className="text-4xl md:text-5xl font-bold text-[#353535] text-center mb-16">
Testimonials
</h2>
<div className="relative flex items-center justify-center">
{/* Navigation Buttons */}
<button
onClick={handlePrev}
className="absolute left-1 xs:left-2 sm:left-3 md:left-4 lg:left-2 xl:-left-16 top-1/2 -translate-y-1/2 z-10 p-1.5 sm:p-2 md:p-2.5 lg:p-2 xl:p-2.5 rounded-full bg-[#353535] text-white hover:bg-[#F48120] transition-colors shadow-md"
aria-label="Previous testimonial"
>
<ChevronLeft className="w-4 h-4 sm:w-5 sm:h-5 md:w-5 md:h-5 lg:w-5 lg:h-5 xl:w-5 xl:h-5" />
</button>
<button
onClick={handleNext}
className="absolute right-1 xs:right-2 sm:right-3 md:right-4 lg:right-2 xl:-right-16 top-1/2 -translate-y-1/2 z-10 p-1.5 sm:p-2 md:p-2.5 lg:p-2 xl:p-2.5 rounded-full bg-[#353535] text-white hover:bg-[#F48120] transition-colors shadow-md"
aria-label="Next testimonial"
>
<ChevronRight className="w-4 h-4 sm:w-5 sm:h-5 md:w-5 md:h-5 lg:w-5 lg:h-5 xl:w-5 xl:h-5" />
</button>
{/* Carousel Viewport */}
<div className="w-full overflow-visible">
{isLoading && (
<div className="text-center py-10 text-[#353535]">Loading testimonials...</div>
)}
{!isLoading && testimonials.length === 0 && (
<div className="text-center py-10 text-[#353535]">
No testimonials available at the moment.
</div>
)}
<div className="flex justify-center lg:grid lg:grid-cols-2 xl:grid-cols-3 gap-6">
<AnimatePresence mode="popLayout">
{visibleTestimonials.map((testimonial, index) => (
<motion.div
key={`${testimonial.id}-${startIndex}`}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className={`h-full w-full max-w-[400px] lg:max-w-none ${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
*/}
<div className="group bg-white p-4 h-full cursor-pointer transition-shadow hover:shadow-lg">
<div className="w-full h-full min-h-[480px] border border-gray-300 group-hover:border-[#F48120] transition-colors duration-300 p-6 md:p-8 flex flex-col items-center text-center">
{/* Profile Image */}
{testimonial.image && (
<div className="w-24 h-24 mb-6 rounded-full overflow-hidden bg-gray-200">
<Image
src={testimonial.image}
alt={testimonial.name}
width={96}
height={96}
className="object-cover"
/>
</div>
)}
{/* Star Rating */}
<div className="flex gap-1 mb-6">
{Array.from({ length: 5 }).map((_, i) => {
const isFilled = i < testimonial.rating
return (
<Star
key={i}
size={18}
className={
isFilled
? "fill-[#F48120] text-[#F48120]"
: "fill-transparent text-[#D1D5DB]"
}
strokeWidth={1.5}
/>
)
})}
</div>
{/* Text - Serif font as per design image */}
<p className="font-serif text-[#353535] text-sm leading-relaxed mb-6 text-justify">
{testimonial.text}
</p>
{/* Divider */}
<div className="w-full h-px bg-gray-300 mb-6 mt-auto"></div>
{/* Author Info */}
<div className="w-full text-left">
<h4 className="font-bold text-[#353535] text-lg mb-1">
{testimonial.name}
</h4>
<div className="text-[#555] text-sm">
<p>{testimonial.title}</p>
<p>{testimonial.organization}</p>
</div>
</div>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
</div>
</section>
)
}