![]() 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, useRef } from 'react';
import { useSession } from 'next-auth/react';
import { motion, AnimatePresence } from 'framer-motion';
import {
MessageSquare,
Reply,
Edit,
Trash2,
User,
Clock,
Send,
Image,
Paperclip,
MoreHorizontal,
CheckCircle,
XCircle
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { useComments } from '@/hooks/useComments';
interface SimpleCommentsProps {
caseId: string;
className?: string;
}
// Utility functions
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getFileIcon = (type: string) => {
if (type.startsWith('image/')) return <Image className="w-4 h-4" />;
if (type.includes('pdf')) return <Paperclip className="w-4 h-4" />;
if (type.includes('word') || type.includes('document')) return <Paperclip className="w-4 h-4" />;
return <Paperclip className="w-4 h-4" />;
};
const SimpleComments: React.FC<SimpleCommentsProps> = ({ caseId, className = '' }) => {
const { data: session } = useSession();
const {
comments,
loading,
posting,
hasMore,
loadingMore,
postComment,
editComment,
deleteComment,
loadMore
} = useComments(caseId);
const [newComment, setNewComment] = useState('');
const [replyingTo, setReplyingTo] = useState<string | null>(null);
const [editingComment, setEditingComment] = useState<string | null>(null);
const [editContent, setEditContent] = useState('');
const [attachments, setAttachments] = useState<File[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
// Handle posting new comment
const handlePostComment = async () => {
if (!newComment.trim() || posting) return;
const success = await postComment(newComment, caseId);
if (success) {
setNewComment('');
setAttachments([]);
}
};
// Handle editing comment
const handleEditComment = async (commentId: string) => {
if (!editContent.trim()) return;
const success = await editComment(commentId, editContent);
if (success) {
setEditContent('');
setEditingComment(null);
}
};
// Handle deleting comment
const handleDeleteComment = async (commentId: string) => {
if (!confirm('Are you sure you want to delete this comment?')) return;
await deleteComment(commentId);
};
// Handle file upload
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
const validFiles = files.filter(file => file.size <= 10 * 1024 * 1024); // 10MB limit
if (validFiles.length !== files.length) {
alert('Some files were too large (max 10MB)');
}
setAttachments(prev => [...prev, ...validFiles]);
};
// Remove attachment
const removeAttachment = (index: number) => {
setAttachments(prev => prev.filter((_, i) => i !== index));
};
// Comment card component
const CommentCard = React.memo(({ comment, depth = 0 }: { comment: any; depth?: number }) => {
const canEdit = session?.user?.id === comment.user.id;
const canDelete = session?.user?.id === comment.user.id || session?.user?.role === 'ADMIN';
const isReplying = replyingTo === comment.id;
const isEditing = editingComment === comment.id;
// Local state for reply input
const [localReplyContent, setLocalReplyContent] = useState('');
const handleLocalPostReply = async () => {
if (!localReplyContent.trim()) return;
const success = await postComment(localReplyContent, caseId, comment.id);
if (success) {
setLocalReplyContent('');
setReplyingTo(null);
}
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`bg-white rounded-lg border border-gray-200 p-4 ${depth > 0 ? 'ml-8 border-l-2 border-blue-200' : ''}`}
>
{/* Comment header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
{comment.user.profilePicture ? (
<img
src={comment.user.profilePicture}
alt={comment.user.name}
className="w-8 h-8 rounded-full object-cover"
/>
) : (
<User className="w-4 h-4 text-blue-600" />
)}
</div>
<div>
<div className="flex items-center space-x-2">
<span className="font-medium text-gray-900">{comment.user.name}</span>
{comment.user.isVerified && (
<CheckCircle className="w-4 h-4 text-blue-500" />
)}
<span className="text-sm text-gray-500 capitalize">{comment.user.role}</span>
</div>
<div className="flex items-center space-x-2 text-xs text-gray-400">
<Clock className="w-3 h-3" />
<span>{formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}</span>
{comment.isEdited && <span>(edited)</span>}
</div>
</div>
</div>
{/* Actions menu */}
{(canEdit || canDelete) && (
<div className="flex items-center space-x-2">
{canEdit && (
<button
onClick={() => {
setEditingComment(comment.id);
setEditContent(comment.content);
}}
className="p-1 hover:bg-gray-100 rounded text-gray-400 hover:text-gray-600"
aria-label="Edit comment"
>
<Edit className="w-4 h-4" />
</button>
)}
{canDelete && (
<button
onClick={() => handleDeleteComment(comment.id)}
className="p-1 hover:bg-gray-100 rounded text-gray-400 hover:text-red-600"
aria-label="Delete comment"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
)}
</div>
{/* Comment content */}
{isEditing ? (
<div className="mb-3">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3}
placeholder="Edit your comment..."
/>
<div className="flex space-x-2 mt-2">
<button
onClick={() => handleEditComment(comment.id)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Save
</button>
<button
onClick={() => {
setEditingComment(null);
setEditContent('');
}}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
) : (
<div className="mb-3">
<p className="text-gray-800 whitespace-pre-wrap">{comment.content}</p>
{/* Attachments */}
{comment.attachments?.length > 0 && (
<div className="mt-3 space-y-2">
{comment.attachments.map((attachment: any) => (
<div key={attachment.id} className="flex items-center space-x-2 p-2 bg-gray-50 rounded">
{getFileIcon(attachment.type)}
<span className="text-sm text-gray-600">{attachment.name}</span>
<span className="text-xs text-gray-400">({formatFileSize(attachment.size)})</span>
<a
href={attachment.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm"
>
View
</a>
</div>
))}
</div>
)}
</div>
)}
{/* Comment actions */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={() => setReplyingTo(isReplying ? null : comment.id)}
className="flex items-center space-x-1 text-gray-500 hover:text-blue-600"
>
<Reply className="w-4 h-4" />
<span className="text-sm">Reply</span>
</button>
</div>
<div className="flex items-center space-x-2 text-sm text-gray-400">
<span>{comment._count?.replies || 0} replies</span>
<span>{comment._count?.reactions || 0} reactions</span>
</div>
</div>
{/* Reply form */}
{isReplying && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4 p-3 bg-gray-50 rounded-lg"
>
<textarea
value={localReplyContent}
onChange={(e) => setLocalReplyContent(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3}
placeholder="Write your reply..."
/>
<div className="flex space-x-2 mt-2">
<button
onClick={handleLocalPostReply}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Reply
</button>
<button
onClick={() => {
setReplyingTo(null);
setLocalReplyContent('');
}}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400"
>
Cancel
</button>
</div>
</motion.div>
)}
</motion.div>
);
});
if (!session) {
return (
<div className={`bg-white rounded-lg border border-gray-200 p-8 text-center ${className}`}>
<MessageSquare className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Sign in to comment</h3>
<p className="text-gray-500">You need to be signed in to view and post comments.</p>
</div>
);
}
return (
<div className={`space-y-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">Comments</h2>
<div className="flex items-center space-x-2 text-sm text-gray-500">
<MessageSquare className="w-4 h-4" />
<span>{comments.length} comments</span>
</div>
</div>
{/* New comment form */}
<div className="bg-white rounded-lg border border-gray-200 p-4">
<div className="flex space-x-3">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0">
{session.user?.image ? (
<img
src={session.user.image}
alt={session.user.name || 'User'}
className="w-8 h-8 rounded-full object-cover"
/>
) : (
<User className="w-4 h-4 text-blue-600" />
)}
</div>
<div className="flex-1">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
rows={3}
placeholder="Write a comment..."
disabled={posting}
/>
{/* Attachments */}
{attachments.length > 0 && (
<div className="mt-2 space-y-1">
{attachments.map((file, index) => (
<div key={index} className="flex items-center space-x-2 p-2 bg-gray-50 rounded">
<Paperclip className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">{file.name}</span>
<span className="text-xs text-gray-400">({formatFileSize(file.size)})</span>
<button
onClick={() => removeAttachment(index)}
className="text-red-500 hover:text-red-700"
>
<XCircle className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
<div className="flex items-center justify-between mt-3">
<div className="flex items-center space-x-2">
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileUpload}
className="hidden"
accept="image/*,.pdf,.doc,.docx"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="p-2 text-gray-400 hover:text-gray-600"
aria-label="Add attachment"
>
<Paperclip className="w-5 h-5" />
</button>
</div>
<button
onClick={handlePostComment}
disabled={!newComment.trim() || posting}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
>
{posting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>Posting...</span>
</>
) : (
<>
<Send className="w-4 h-4" />
<span>Post Comment</span>
</>
)}
</button>
</div>
</div>
</div>
</div>
{/* Comments list */}
<div className="space-y-4">
{loading ? (
<div className="text-center py-8">
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500">Loading comments...</p>
</div>
) : comments.length === 0 ? (
<div className="text-center py-8">
<MessageSquare className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No comments yet</h3>
<p className="text-gray-500">Be the first to share your thoughts!</p>
</div>
) : (
<>
<AnimatePresence>
{comments.map((comment) => (
<CommentCard key={comment.id} comment={comment} />
))}
</AnimatePresence>
{/* Load more */}
{hasMore && (
<div className="text-center pt-4">
<button
onClick={loadMore}
disabled={loadingMore}
className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50"
>
{loadingMore ? 'Loading...' : 'Load More Comments'}
</button>
</div>
)}
</>
)}
</div>
</div>
);
};
export default SimpleComments;