![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/public_html/src/components/ |
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Star,
Quote,
ThumbsUp,
MessageCircle,
Calendar,
User,
Heart,
Award,
Shield,
TrendingUp
} from 'lucide-react';
import Image from 'next/image';
import { formatDistanceToNow } from 'date-fns';
interface Testimonial {
id: string;
author: {
id: string;
name: string;
username: string;
profilePicture?: string;
role: string;
};
content: string;
rating: number;
type: 'testimonial' | 'review' | 'endorsement';
category?: string;
timestamp: string;
helpful: number;
isVerified: boolean;
caseType?: string;
outcome?: string;
}
interface ProfileTestimonialsProps {
profileId: string;
profileName: string;
isOwnProfile: boolean;
}
const ProfileTestimonials: React.FC<ProfileTestimonialsProps> = ({
profileId,
profileName,
isOwnProfile
}) => {
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [loading, setLoading] = useState(true);
const [activeFilter, setActiveFilter] = useState<'all' | 'testimonials' | 'reviews' | 'endorsements'>('all');
const [showAll, setShowAll] = useState(false);
useEffect(() => {
fetchTestimonials();
}, [profileId]);
const fetchTestimonials = async () => {
try {
const response = await fetch(`/api/profile/${profileId}/testimonials`);
if (response.ok) {
const data = await response.json();
setTestimonials(data.testimonials || []);
}
} catch (error) {
console.error('Error fetching testimonials:', error);
} finally {
setLoading(false);
}
};
const handleHelpful = async (testimonialId: string) => {
try {
const response = await fetch(`/api/testimonials/${testimonialId}/helpful`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
setTestimonials(prev =>
prev.map(t =>
t.id === testimonialId
? { ...t, helpful: t.helpful + 1 }
: t
)
);
}
} catch (error) {
console.error('Error marking helpful:', error);
}
};
const filteredTestimonials = testimonials.filter(testimonial => {
if (activeFilter === 'all') return true;
// Map filter keys to testimonial types
const typeMap: Record<string, string> = {
'testimonials': 'testimonial',
'reviews': 'review',
'endorsements': 'endorsement'
};
return testimonial.type === typeMap[activeFilter];
});
const displayedTestimonials = showAll ? filteredTestimonials : filteredTestimonials.slice(0, 3);
const getTestimonialIcon = (type: string) => {
switch (type) {
case 'testimonial':
return <Quote className="h-4 w-4 text-blue-500" />;
case 'review':
return <Star className="h-4 w-4 text-yellow-500" />;
case 'endorsement':
return <ThumbsUp className="h-4 w-4 text-green-500" />;
default:
return <MessageCircle className="h-4 w-4 text-gray-500" />;
}
};
const getTestimonialColor = (type: string) => {
switch (type) {
case 'testimonial':
return 'bg-blue-50 border-blue-200';
case 'review':
return 'bg-yellow-50 border-yellow-200';
case 'endorsement':
return 'bg-green-50 border-green-200';
default:
return 'bg-gray-50 border-gray-200';
}
};
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`h-4 w-4 ${
i < rating ? 'text-yellow-400 fill-current' : 'text-gray-300'
}`}
/>
));
};
if (loading) {
return (
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
<div className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center space-x-3 mb-3">
<div className="h-10 w-10 bg-gray-200 rounded-full"></div>
<div className="flex-1">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/4"></div>
</div>
</div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
))}
</div>
</div>
</div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-xl shadow-lg border border-gray-100"
>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Award className="h-5 w-5 mr-2 text-purple-600" />
Testimonials & Reviews
</h3>
<div className="text-sm text-gray-600">
{testimonials.length} total
</div>
</div>
{/* Filter Tabs */}
<div className="flex space-x-1">
{[
{ key: 'all', label: 'All', count: testimonials.length },
{ key: 'testimonials', label: 'Testimonials', count: testimonials.filter(t => t.type === 'testimonial').length },
{ key: 'reviews', label: 'Reviews', count: testimonials.filter(t => t.type === 'review').length },
{ key: 'endorsements', label: 'Endorsements', count: testimonials.filter(t => t.type === 'endorsement').length }
].map(filter => (
<button
key={filter.key}
onClick={() => setActiveFilter(filter.key as any)}
className={`px-3 py-1 text-sm rounded-lg transition-colors ${
activeFilter === filter.key
? 'bg-purple-100 text-purple-700'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{filter.label} ({filter.count})
</button>
))}
</div>
</div>
{/* Testimonials List */}
<div className="p-6">
<AnimatePresence mode="wait">
{displayedTestimonials.length > 0 ? (
<motion.div
key={activeFilter}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="space-y-4"
>
{displayedTestimonials.map((testimonial, index) => (
<motion.div
key={testimonial.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className={`border rounded-lg p-4 ${getTestimonialColor(testimonial.type)}`}
>
{/* Author Info */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
{testimonial.author.profilePicture ? (
<Image
src={testimonial.author.profilePicture}
alt={testimonial.author.name}
width={40}
height={40}
className="rounded-full"
/>
) : (
<div className="w-10 h-10 bg-gradient-to-br from-purple-400 to-pink-400 rounded-full flex items-center justify-center text-white font-semibold">
{testimonial.author.name.charAt(0)}
</div>
)}
<div>
<div className="flex items-center space-x-2">
<h4 className="font-medium text-gray-900">
{testimonial.author.name}
</h4>
{testimonial.isVerified && (
<Shield className="h-4 w-4 text-blue-500" />
)}
</div>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<span>{testimonial.author.role}</span>
<span>•</span>
<span>{formatDistanceToNow(new Date(testimonial.timestamp), { addSuffix: true })}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{getTestimonialIcon(testimonial.type)}
{testimonial.rating > 0 && (
<div className="flex items-center space-x-1">
{renderStars(testimonial.rating)}
</div>
)}
</div>
</div>
{/* Content */}
<div className="mb-3">
<p className="text-gray-700 leading-relaxed">
"{testimonial.content}"
</p>
</div>
{/* Case Details */}
{testimonial.caseType && (
<div className="mb-3 p-3 bg-white rounded-lg border border-gray-200">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Case Type:</span>
<span className="font-medium text-gray-900">{testimonial.caseType}</span>
</div>
{testimonial.outcome && (
<div className="flex items-center justify-between text-sm mt-1">
<span className="text-gray-600">Outcome:</span>
<span className={`font-medium ${
testimonial.outcome.toLowerCase().includes('won')
? 'text-green-600'
: 'text-gray-900'
}`}>
{testimonial.outcome}
</span>
</div>
)}
</div>
)}
{/* Actions */}
<div className="flex items-center justify-between pt-3 border-t border-gray-200">
<button
onClick={() => handleHelpful(testimonial.id)}
className="flex items-center space-x-1 text-sm text-gray-600 hover:text-blue-600 transition-colors"
>
<ThumbsUp className="h-4 w-4" />
<span>Helpful ({testimonial.helpful})</span>
</button>
<div className="flex items-center space-x-2 text-xs text-gray-500">
{testimonial.category && (
<span className="px-2 py-1 bg-gray-100 rounded-full">
{testimonial.category}
</span>
)}
</div>
</div>
</motion.div>
))}
</motion.div>
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-8"
>
<Award className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">
No {activeFilter === 'all' ? 'testimonials' : activeFilter} yet
</p>
{!isOwnProfile && (
<p className="text-sm text-gray-400 mt-2">
Be the first to leave a review for {profileName}
</p>
)}
</motion.div>
)}
</AnimatePresence>
{/* Show More/Less */}
{filteredTestimonials.length > 3 && (
<div className="mt-6 text-center">
<button
onClick={() => setShowAll(!showAll)}
className="px-4 py-2 text-sm font-medium text-purple-600 hover:text-purple-700 transition-colors"
>
{showAll ? 'Show Less' : `Show ${filteredTestimonials.length - 3} More`}
</button>
</div>
)}
</div>
{/* Summary Stats */}
{testimonials.length > 0 && (
<div className="border-t border-gray-200 p-6">
<h4 className="text-sm font-medium text-gray-900 mb-3">Summary</h4>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-lg font-bold text-yellow-600">
{(testimonials.reduce((acc, t) => acc + t.rating, 0) / testimonials.filter(t => t.rating > 0).length).toFixed(1)}
</div>
<div className="text-xs text-gray-600">Avg Rating</div>
</div>
<div>
<div className="text-lg font-bold text-green-600">
{testimonials.filter(t => t.type === 'endorsement').length}
</div>
<div className="text-xs text-gray-600">Endorsements</div>
</div>
<div>
<div className="text-lg font-bold text-blue-600">
{testimonials.filter(t => t.isVerified).length}
</div>
<div className="text-xs text-gray-600">Verified</div>
</div>
</div>
</div>
)}
</motion.div>
);
};
export default ProfileTestimonials;