157 lines
6.5 KiB
TypeScript
157 lines
6.5 KiB
TypeScript
"use client";
|
|
import React, { useEffect, useState } from "react";
|
|
import { Star, MessageSquareQuote, ShieldCheck, UserCheck } from "lucide-react";
|
|
import { motion } from "motion/react";
|
|
import SafeImage from "./SafeImage";
|
|
import { useSettingsStore } from "../lib/store/settingsStore";
|
|
import api, { BASE_DOMAIN } from "../lib/services/api";
|
|
|
|
export interface Testimonial {
|
|
id: string;
|
|
authorName: string;
|
|
roleTitle?: string;
|
|
avatarUrl?: string;
|
|
content: string;
|
|
rating: number;
|
|
isFeatured: boolean;
|
|
isActive: boolean;
|
|
}
|
|
|
|
export default function TestimonialsSection() {
|
|
const getText = useSettingsStore(state => state.getText);
|
|
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function loadTestimonials() {
|
|
try {
|
|
setIsLoading(true);
|
|
const res = await api.get('/testimonials/homepage');
|
|
const raw = res.data;
|
|
const list = Array.isArray(raw?.testimonials)
|
|
? raw.testimonials
|
|
: Array.isArray(raw?.data?.testimonials)
|
|
? raw.data.testimonials
|
|
: Array.isArray(raw?.data)
|
|
? raw.data
|
|
: Array.isArray(raw)
|
|
? raw
|
|
: [];
|
|
setTestimonials(list.filter((t: Testimonial) => t.isActive !== false));
|
|
} catch (err) {
|
|
console.error('Failed to load testimonials:', err);
|
|
setTestimonials([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
loadTestimonials();
|
|
|
|
}, []);
|
|
|
|
if (!isLoading && testimonials.length === 0) {
|
|
return null; // Gracefully hide section if no testimonials exist in backend
|
|
}
|
|
|
|
return (
|
|
<section className="py-8 sm:py-20 bg-medical-gray-50/50 border-t border-medical-gray-100 font-vazir" dir="rtl">
|
|
<div className="max-w-7xl mx-auto px-4">
|
|
{/* Section Header */}
|
|
<div className="text-center max-w-2xl mx-auto mb-6 sm:mb-16">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
className="inline-flex items-center gap-2 px-3.5 py-1.5 bg-canina-blue/10 text-canina-blue rounded-full text-[10px] font-black uppercase tracking-widest mb-3 sm:mb-4"
|
|
>
|
|
<UserCheck className="w-3.5 h-3.5" />
|
|
<span>{getText('testimonials_badge', 'توصیهها و تجربیات')}</span>
|
|
</motion.div>
|
|
<h2 className="text-2xl sm:text-4xl md:text-5xl font-black text-medical-gray-900 leading-tight font-lalezar tracking-wide">
|
|
{getText('testimonials_title_1', 'نظرات')} <span className="text-canina-blue">{getText('testimonials_title_2', 'دامپزشکان و مشتریان')}</span>
|
|
</h2>
|
|
<p className="text-sm sm:text-lg text-medical-gray-500 font-medium mt-2 sm:mt-4">
|
|
{getText('testimonials_desc', 'تجربه استفاده از مکملهای دارویی کنینا از نگاه برترین کلینیکهای دامپزشکی و سرپرستان پت در سراسر کشور.')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Testimonials Grid */}
|
|
{isLoading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="bg-white rounded-3xl p-6 border border-medical-gray-100 shadow-xs animate-pulse space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 rounded-2xl bg-medical-gray-100" />
|
|
<div className="space-y-2 flex-1">
|
|
<div className="h-4 bg-medical-gray-100 rounded-md w-1/2" />
|
|
<div className="h-3 bg-medical-gray-100 rounded-md w-1/3" />
|
|
</div>
|
|
</div>
|
|
<div className="h-16 bg-medical-gray-100 rounded-xl" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{testimonials.map((item, index) => {
|
|
const avatar = item.avatarUrl?.startsWith('http')
|
|
? item.avatarUrl
|
|
: (item.avatarUrl ? `${BASE_DOMAIN}${item.avatarUrl}` : '');
|
|
|
|
return (
|
|
<motion.div
|
|
key={item.id || index}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="bg-white rounded-3xl p-6 border border-medical-gray-100 shadow-sm hover:shadow-md transition-all flex flex-col justify-between"
|
|
>
|
|
<div>
|
|
{/* Rating stars */}
|
|
<div className="flex items-center gap-1 text-amber-400 mb-4">
|
|
{Array.from({ length: item.rating || 5 }).map((_, r) => (
|
|
<Star key={r} className="w-4 h-4 fill-amber-400" />
|
|
))}
|
|
</div>
|
|
|
|
{/* Content Quote */}
|
|
<p className="text-sm text-medical-gray-700 leading-relaxed font-medium mb-6 relative">
|
|
«{item.content}»
|
|
</p>
|
|
</div>
|
|
|
|
{/* Author footer */}
|
|
<div className="flex items-center gap-3 pt-4 border-t border-medical-gray-100">
|
|
<div className="w-12 h-12 rounded-2xl bg-medical-gray-50 border border-medical-gray-100 flex items-center justify-center overflow-hidden flex-shrink-0">
|
|
{avatar ? (
|
|
<SafeImage
|
|
src={avatar}
|
|
alt={item.authorName}
|
|
className="w-full h-full"
|
|
imgClassName="object-cover"
|
|
/>
|
|
) : (
|
|
<UserCheck className="w-6 h-6 text-canina-blue/40" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-extrabold text-sm text-medical-gray-900 leading-tight">
|
|
{item.authorName}
|
|
</h3>
|
|
{item.roleTitle && (
|
|
<p className="text-xs text-canina-blue font-bold mt-0.5">
|
|
{item.roleTitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|