canina/frontend/application/components/TestimonialsSection.tsx
parsa aghaei 2c452215c3
All checks were successful
Deploy Canina / deploy (push) Successful in 1m41s
feat: resolve doctors query error, add FAQ, testimonials, blog preview, and full menu management
2026-08-19 17:20:01 +03:30

153 lines
6.3 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-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-16">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-amber-500/10 text-amber-600 rounded-full text-xs font-black uppercase tracking-widest mb-4">
<MessageSquareQuote className="w-4 h-4" />
<span>{getText('testimonials_badge', "گواهی کیفیت و اصالت")}</span>
</div>
<h2 className="text-3xl md:text-4xl font-black text-medical-gray-900 font-lalezar leading-tight">
{getText('testimonials_title', "دیدگاه دامپزشکان و همراهان")} <span className="text-canina-blue">{getText('testimonials_title_highlight', "کنینا")}</span>
</h2>
<p className="text-base text-medical-gray-500 font-medium mt-4">
{getText('testimonials_subtitle', "تجربه متخصصان، کلینیک‌های همکار و سرپرستان حیوانات خانگی از نتایج مکمل‌های آلمانی کنینا")}
</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>
<h4 className="font-extrabold text-sm text-medical-gray-900 leading-tight">
{item.authorName}
</h4>
{item.roleTitle && (
<p className="text-xs text-canina-blue font-bold mt-0.5">
{item.roleTitle}
</p>
)}
</div>
</div>
</motion.div>
);
})}
</div>
)}
</div>
</section>
);
}