T.ME/BIBIL_0DAY
CasperSecurity


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.quebec/private_html/src/components/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/lavocat.quebec/private_html/src/components/ProfileTestimonials.tsx
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(
      if (response.ok) {
        const data = await response.json();
        setTestimonials(data.testimonials || []);
      }
    } catch (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) {
      }
  };

  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={
          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={
                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={
                            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; 

CasperSecurity Mini