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/CommentsSystem.tsx
import React, { useState, useEffect, useRef, useCallback } 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,
  Heart,
  ThumbsUp,
  AlertTriangle,
  CheckCircle,
  XCircle
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import { format, formatDistanceToNow } from 'date-fns';

// Types
interface CommentUser {
  id: string;
  name: string;
  profilePicture?: string;
  role: string;
  isVerified?: boolean;
}

interface CommentAttachment {
  id: string;
  name: string;
  url: string;
  type: string;
  size: number;
}

interface CommentReaction {
  id: string;
  reactionType: string;
  user: {
    id: string;
    name: string;
  };
}

interface Comment {
  id: string;
  content: string;
  createdAt: string;
  updatedAt: string;
  isEdited: boolean;
  isDeleted: boolean;
  user: CommentUser;
  attachments: CommentAttachment[];
  reactions: CommentReaction[];
  _count: {
    replies: number;
    reactions: number;
  };
}

interface CommentsSystemProps {
  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 CommentsSystem: React.FC<CommentsSystemProps> = ({ caseId, className = '' }) => {
  const { data: session } = useSession();
  const [comments, setComments] = useState<Comment[]>([]);
  const [loading, setLoading] = useState(true);
  const [posting, setPosting] = useState(false);
  const [newComment, setNewComment] = useState('');
  const [replyingTo, setReplyingTo] = useState<string | null>(null);
  const [replyContent, setReplyContent] = useState('');
  const [editingComment, setEditingComment] = useState<string | null>(null);
  const [editContent, setEditContent] = useState('');
  const [showReplies, setShowReplies] = useState<Set<string>>(new Set());
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [attachments, setAttachments] = useState<File[]>([]);
  const [deletingId, setDeletingId] = useState<string | null>(null);
  
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  // Fetch comments
  const fetchComments = useCallback(async (reset = false) => {
    try {
      if (reset) {
        setPage(1);
        setComments([]);
        setHasMore(true);
      }
      
      setLoading(true);
      const params = new URLSearchParams({
        caseId,
        page: reset ? '1' : String(page),
        limit: '20'
      });
      
      const response = await fetch(
      if (!response.ok) throw new Error('Failed to fetch comments');
      
      const data = await response.json();
      const newComments = data.comments;
      
      if (reset) {
        setComments(newComments);
      } else {
        setComments(prev => {
          const existingIds = new Set(prev.map(c => c.id));
          const uniqueNewComments = newComments.filter((c: Comment) => !existingIds.has(c.id));
          return [...prev, ...uniqueNewComments];
        });
      }
      
      setHasMore(data.pagination.page < data.pagination.pages);
    } catch (error) {
      toast.error('Failed to load comments');
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  }, [caseId, page]);

  // Load more comments
  const loadMore = useCallback(async () => {
    if (loadingMore || !hasMore) return;
    
    setLoadingMore(true);
    setPage(prev => prev + 1);
  }, [loadingMore, hasMore]);

  // Initial load
  useEffect(() => {
    fetchComments(true);
  }, [caseId]);

  // Load more when page changes
  useEffect(() => {
    if (page > 1) {
      fetchComments(false);
    }
  }, [page]);

  // Post comment
  const handlePostComment = async () => {
    if (!newComment.trim() || posting) return;
    setPosting(true);
    try {
      const response = await fetch('/api/comments', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          content: newComment.trim(),
          caseId
        })
      });
      if (!response.ok) {
        const err = await response.json();
        toast.error(err.error || 'Failed to post comment');
        return;
      }
      const data = await response.json();
      setComments(prev => [data.comment, ...prev]);
      setNewComment('');
      setAttachments([]);
      toast.success('Comment posted successfully');
    } catch (error) {
      toast.error('Failed to post comment');
    } finally {
      setPosting(false);
    }
  };

  // Post reply
  const handlePostReply = async (parentId: string) => {
    if (!replyContent.trim()) return;
    
    try {
      const response = await fetch('/api/comments', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          content: replyContent.trim(),
          caseId,
          parentId
        })
      });

      if (!response.ok) throw new Error('Failed to post reply');
      
      const data = await response.json();
      
      // Update comments to include the new reply
      setComments(prev => prev.map(comment => {
        if (comment.id === parentId) {
          return {
            ...comment,
            _count: {
              ...comment._count,
              replies: comment._count.replies + 1
            }
          };
        }
        return comment;
      }));
      
      setReplyContent('');
      setReplyingTo(null);
      
      toast.success('Reply posted successfully');
    } catch (error) {
      toast.error('Failed to post reply');
    }
  };

  // Edit comment
  const handleEditComment = async (commentId: string) => {
    if (!editContent.trim()) return;
    
    try {
      const response = await fetch(`/api/comments?commentId=${commentId}
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          content: editContent.trim()
        })
      });

      if (!response.ok) throw new Error('Failed to update comment');
      
      const data = await response.json();
      
      // Update comment in state
      setComments(prev => prev.map(comment => 
        comment.id === commentId ? data.comment : comment
      ));
      
      setEditContent('');
      setEditingComment(null);
      
      toast.success('Comment updated successfully');
    } catch (error) {
      toast.error('Failed to update comment');
    }
  };

  // Delete comment
  const handleDeleteComment = async (commentId: string) => {
    if (!confirm('Are you sure you want to delete this comment?')) return;
    setDeletingId(commentId);
    // Optimistically remove from UI
    setComments(prev => prev.filter(comment => comment.id !== commentId));
    try {
      const response = await fetch(`/api/comments?commentId=${commentId}
      if (!response.ok) {
        const error = await response.json();
        if (
          error.error && (
            error.error.includes('already been deleted') ||
            error.error.includes('not found')
          )
        ) {
          toast.success('Comment removed from UI.');
          await fetchComments(true);
          setDeletingId(null);
          return;
        }
        throw new Error('Failed to delete comment');
      }
      toast.success('Comment deleted successfully');
      await fetchComments(true);
    } catch (error) {
      toast.error('Failed to delete comment');
      await fetchComments(true); // Restore correct state if error
    } finally {
      setDeletingId(null);
    }
  };

  // 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) {
      toast.error('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.FC<{
    comment: Comment;
    depth?: number;
    isReply?: boolean;
  }> = ({ comment, depth = 0, isReply = false }) => {
    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;

    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="relative">
              <button
                onClick={() => {
                  if (canEdit) {
                    setEditingComment(comment.id);
                    setEditContent(comment.content);
                  }
                }}
                className="p-1 hover:bg-gray-100 rounded"
                aria-label="Comment actions"
              >
                <MoreHorizontal className="w-4 h-4 text-gray-400" />
              </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) => (
                  <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>
            
            {canDelete && (
              <button
                onClick={() => handleDeleteComment(comment.id)}
                className="flex items-center space-x-1 text-gray-500 hover:text-red-600"
                disabled={deletingId === comment.id}
              >
                {deletingId === comment.id ? (
                  <span className="w-4 h-4 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin inline-block"></span>
                ) : (
                  <Trash2 className="w-4 h-4" />
                )}
                <span className="text-sm">Delete</span>
              </button>
            )}
          </div>
          
          <div className="flex items-center space-x-2 text-sm text-gray-400">
            <span>{comment._count.replies} replies</span>
            <span>{comment._count.reactions} 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={replyContent}
              onChange={(e) => setReplyContent(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={() => handlePostReply(comment.id)}
                className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
              >
                Reply
              </button>
              <button
                onClick={() => {
                  setReplyingTo(null);
                  setReplyContent('');
                }}
                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
              ref={textareaRef}
              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.filter(comment => !comment.isDeleted).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 CommentsSystem; 

CasperSecurity Mini