![]() 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/ |
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useSession } from 'next-auth/react';
import toast from 'react-hot-toast';
interface UserProfile {
id: string;
email: string;
name: string;
role: string;
profilePicture?: string;
bio?: string;
title?: string;
specialization?: string;
yearsOfExperience?: number;
education?: string;
officeLocation?: string;
linkedinUrl?: string;
websiteUrl?: string;
availability?: string;
timezone?: string;
pronouns?: string;
isProfilePublic: boolean;
lastActive?: string;
language: string;
createdAt: string;
}
interface ProfilePopoverProps {
userId: string;
isOpen: boolean;
onClose: () => void;
position?: 'left' | 'right' | 'center';
onStartDirectMessage?: (userId: string, userName: string) => void;
}
const ProfilePopover: React.FC<ProfilePopoverProps> = ({
userId,
isOpen,
onClose,
position = 'center',
onStartDirectMessage
}) => {
const { data: session } = useSession();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isOpen && userId) {
fetchProfile();
}
}, [isOpen, userId]);
const fetchProfile = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(
if (response.ok) {
const data = await response.json();
setProfile(data);
} else if (response.status === 403) {
setError('This profile is private');
} else {
setError('Failed to load profile');
}
} catch (error) {
setError('Error loading profile');
} finally {
setLoading(false);
}
};
const getRoleIcon = (role: string) => {
switch (role) {
case 'ADMIN':
return '⚖️';
case 'LAWYER':
return '👩⚖️';
default:
return '👤';
}
};
const getRoleName = (role: string) => {
switch (role) {
case 'ADMIN':
return 'Legal Team';
case 'LAWYER':
return 'Lawyer';
default:
return 'Client';
}
};
const getStatusColor = (availability?: string) => {
switch (availability) {
case 'Available':
return 'bg-green-400';
case 'Busy':
return 'bg-yellow-400';
case 'Away':
return 'bg-orange-400';
case 'Do Not Disturb':
return 'bg-red-400';
default:
return 'bg-gray-400';
}
};
const isOnline = (lastActive?: string) => {
if (!lastActive) return false;
const lastActiveDate = new Date(lastActive);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
return lastActiveDate > fiveMinutesAgo;
};
const formatLastActive = (lastActive?: string) => {
if (!lastActive) return 'Unknown';
const date = new Date(lastActive);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
if (diffInMinutes < 1) return 'Just now';
if (diffInMinutes < 60) return `${diffInMinutes}m ago
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago
return date.toLocaleDateString();
};
const getInitials = (name: string) => {
return name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
};
const handleStartDirectMessage = () => {
if (profile && onStartDirectMessage) {
onStartDirectMessage(profile.id, profile.name);
onClose();
}
};
const copyEmail = () => {
if (profile?.email) {
navigator.clipboard.writeText(profile.email);
toast.success('Email copied to clipboard');
}
};
const openLinkedIn = () => {
if (profile?.linkedinUrl) {
window.open(profile.linkedinUrl, '_blank');
}
};
const openWebsite = () => {
if (profile?.websiteUrl) {
window.open(profile.websiteUrl, '_blank');
}
};
const positionClasses = {
left: 'right-0',
right: 'left-0',
center: 'left-1/2 transform -translate-x-1/2'
};
if (!isOpen) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 10 }}
className={`absolute top-full mt-2 ${positionClasses[position]} bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-80 max-w-sm
style={{ zIndex: 99999 }}
>
{/* Header */}
<div className="flex justify-between items-start p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
User Profile
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="p-4">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
) : error ? (
<div className="text-center py-8">
<div className="text-gray-400 text-4xl mb-2">🔒</div>
<p className="text-gray-600 dark:text-gray-400">{error}</p>
</div>
) : profile ? (
<div className="space-y-4">
{/* Avatar and basic info */}
<div className="flex items-start space-x-4">
<div className="relative">
<div className="w-16 h-16 rounded-full overflow-hidden bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
{profile.profilePicture ? (
<img
src={profile.profilePicture}
alt={profile.name}
className="w-full h-full object-cover"
/>
) : (
<span className="text-white font-semibold text-xl">
{getInitials(profile.name)}
</span>
)}
</div>
{/* Status indicator */}
<div className="absolute -bottom-1 -right-1">
<div
className={
isOnline(profile.lastActive) ? 'bg-green-400' : getStatusColor(profile.availability)
}
/>
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<h4 className="font-semibold text-gray-900 dark:text-white truncate">
{profile.name}
</h4>
<span className="text-lg">{getRoleIcon(profile.role)}</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
{getRoleName(profile.role)}
</p>
{profile.title && (
<p className="text-sm text-gray-500 dark:text-gray-500 truncate">
{profile.title}
</p>
)}
<div className="flex items-center space-x-2 mt-1">
<div
className={
isOnline(profile.lastActive) ? 'bg-green-400' : getStatusColor(profile.availability)
}
/>
<span className="text-xs text-gray-500 dark:text-gray-500">
{isOnline(profile.lastActive)
? 'Online'
: profile.availability || formatLastActive(profile.lastActive)
}
</span>
</div>
</div>
</div>
{/* Bio */}
{profile.bio && (
<div>
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">About</h5>
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed">
{profile.bio}
</p>
</div>
)}
{/* Professional info */}
{(profile.specialization || profile.yearsOfExperience || profile.education) && (
<div>
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Professional</h5>
<div className="space-y-1">
{profile.specialization && (
<div className="flex items-center text-sm">
<span className="text-gray-500 dark:text-gray-400 w-20">Specialty:</span>
<span className="text-gray-700 dark:text-gray-300">{profile.specialization}</span>
</div>
)}
{profile.yearsOfExperience && (
<div className="flex items-center text-sm">
<span className="text-gray-500 dark:text-gray-400 w-20">Experience:</span>
<span className="text-gray-700 dark:text-gray-300">{profile.yearsOfExperience} years</span>
</div>
)}
{profile.officeLocation && (
<div className="flex items-center text-sm">
<span className="text-gray-500 dark:text-gray-400 w-20">Location:</span>
<span className="text-gray-700 dark:text-gray-300">{profile.officeLocation}</span>
</div>
)}
</div>
</div>
)}
{/* Contact and links */}
<div className="space-y-2">
<div className="flex items-center space-x-2">
<button
onClick={copyEmail}
className="flex items-center space-x-2 text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
<span>{profile.email}</span>
</button>
</div>
<div className="flex items-center space-x-3">
{profile.linkedinUrl && (
<button
onClick={openLinkedIn}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
title="LinkedIn Profile"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</button>
)}
{profile.websiteUrl && (
<button
onClick={openWebsite}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
title="Website"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9m0 9c-5 0-9-4-9-9s4-9 9-9" />
</svg>
</button>
)}
</div>
</div>
{/* Actions */}
{session?.user?.id !== profile.id && (
<div className="pt-3 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleStartDirectMessage}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center justify-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span>Send Message</span>
</button>
</div>
)}
</div>
) : (
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">No profile data available</p>
</div>
)}
</div>
</motion.div>
</AnimatePresence>
);
};
export default ProfilePopover;