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

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/lavocat.ca/private_html/src/components/ThreadedComments.tsx
import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import { motion, AnimatePresence } from 'framer-motion';
import { 
  MessageSquare, 
  ThumbsUp, 
  Reply, 
  MoreHorizontal, 
  Edit, 
  Trash2,
  User,
  Clock,
  Heart
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import { format } from 'date-fns';
import ReactionPicker from './ReactionPicker';

interface CommentUser {
  id: string;
  name: string;
  profilePicture?: string;
  role: string;
}

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

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

interface ThreadedCommentsProps {
  caseId: string;
  initialComments?: Comment[];
  onCommentAdded?: () => void;
}

const ThreadedComments: React.FC<ThreadedCommentsProps> = ({
  caseId,
  initialComments = [],
  onCommentAdded
}) => {
  const { data: session } = useSession();
  const [comments, setComments] = useState<Comment[]>(initialComments);
  const [loading, setLoading] = useState(false);
  const [posting, setPosting] = useState(false);
  const [newComment, setNewComment] = useState('');
  const [replyingTo, setReplyingTo] = useState<string | null>(null);
  const [replyContent, setReplyContent] = useState<{ [commentId: string]: string }>({});
  const [editingComment, setEditingComment] = useState<string | null>(null);
  const [editContent, setEditContent] = useState('');
  const [showReplies, setShowReplies] = useState<Set<string>>(new Set());
  const [likedComments, setLikedComments] = useState<Set<string>>(new Set());
  // Refs to preserve state across fetches
  const replyingToRef = useRef<string | null>(null);
  const replyContentRef = useRef<{ [commentId: string]: string }>({});

  // Utility function to normalize comment data
  const normalizeComment = (comment: any): Comment => ({
    ...comment,
    reactions: comment.reactions ?? [],
    replies: (comment.replies ?? []).map((reply: any) => ({
      ...reply,
      reactions: reply.reactions ?? [],
      _count: {
        replies: reply._count?.replies || reply.replies?.length || 0,
        likedBy: reply._count?.likedBy || 0,
        reactions: reply._count?.reactions || reply.reactions?.length || 0
      }
    })),
    _count: {
      replies: comment._count?.replies || comment.replies?.length || 0,
      likedBy: comment._count?.likedBy || 0,
      reactions: comment._count?.reactions || comment.reactions?.length || 0
    }
  });

  // Keep refs in sync - but only when actually needed
  useEffect(() => { 
    if (replyingTo !== replyingToRef.current) {
      replyingToRef.current = replyingTo; 
    }
  }, [replyingTo]);
  
  useEffect(() => { 
    if (JSON.stringify(replyContent) !== JSON.stringify(replyContentRef.current)) {
      replyContentRef.current = replyContent; 
    }
  }, [replyContent]);

  useEffect(() => {
    fetchComments();
  }, [caseId]);

  const fetchComments = async () => {
    try {
      setLoading(true);
      const response = await fetch(`/api/live-cases/${caseId}/comments`);
      if (response.ok) {
        const data = await response.json();
        // Normalize comments to ensure reactions are always present
        setComments(data.comments.map(normalizeComment));
        // Restore reply state after fetch
        setReplyingTo(replyingToRef.current);
        setReplyContent(replyContentRef.current);
      } else {
        console.error('Failed to fetch comments');
      }
    } catch (error) {
      console.error('Error fetching comments:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleAddComment = useCallback(async () => {
    if (!session) {
      toast.error('Please login to comment');
      return;
    }

    if (!newComment.trim()) {
      toast.error('Please enter a comment');
      return;
    }

    try {
      setPosting(true);
      const response = await fetch(`/api/live-cases/${caseId}/comments`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: newComment })
      });

      if (response.ok) {
        const data = await response.json();
        setComments(prev => [normalizeComment(data.comment), ...prev]);
        setNewComment('');
        toast.success('Comment added! 💬');
        onCommentAdded?.();
      } else {
        toast.error('Failed to add comment');
      }
    } catch (error) {
      toast.error('Error adding comment');
    } finally {
      setPosting(false);
    }
  }, [session, newComment, caseId, onCommentAdded]);

  const handleAddReply = useCallback(async (parentId: string) => {
    if (!session) {
      toast.error('Please login to reply');
      return;
    }

    if (!replyContent[parentId] || !replyContent[parentId].trim()) {
      toast.error('Please enter a reply');
      return;
    }

    try {
      const response = await fetch(`/api/live-cases/${caseId}/comments`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
          content: replyContent[parentId],
          parentId: parentId
        })
      });

      if (response.ok) {
        const data = await response.json();
        setComments(prev => prev.map(comment => {
          if (comment.id === parentId) {
            return {
              ...comment,
              replies: [...comment.replies, normalizeComment(data.comment)],
              _count: {
                ...comment._count,
                replies: (comment._count?.replies || comment.replies?.length || 0) + 1
              }
            };
          }
          return comment;
        }));
        setReplyContent(prev => ({ ...prev, [parentId]: '' }));
        setReplyingTo(null);
        // Ensure replies are shown after adding a reply
        setShowReplies(prev => new Set(Array.from(prev).concat(parentId)));
        toast.success('Reply added! 💬');
        onCommentAdded?.();
      } else {
        toast.error('Failed to add reply');
      }
    } catch (error) {
      toast.error('Error adding reply');
    }
  }, [session, replyContent, caseId, onCommentAdded]);

  const handleReaction = useCallback(async (commentId: string, reactionType: string) => {
    if (!session) {
      toast.error('Please login to react to comments');
      return;
    }

    try {
      const response = await fetch(`/api/live-cases/${caseId}/comments/${commentId}/reactions`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ reactionType })
      });

      if (response.ok) {
        const data = await response.json();
        
        // Determine if reaction was added or removed based on message
        const wasAdded = data.message === 'Reaction added';
        
        // Update the comment's reactions
        setComments(prev => prev.map(comment => {
          if (comment.id === commentId) {
            return updateCommentReactions(comment, reactionType, wasAdded);
          }
          // Also update replies
          return {
            ...comment,
            replies: comment.replies.map(reply => {
              if (reply.id === commentId) {
                return updateCommentReactions(reply, reactionType, wasAdded);
              }
              return reply;
            })
          };
        }));

        const reactionEmojis: { [key: string]: string } = {
          like: '👍', love: '❤️', laugh: '😂', wow: '😮', sad: '😢', angry: '😠',
          dislike: '👎', star: '⭐', justice: '⚖️', gavel: '🔨', shield: '🛡️'
        };

        toast.success(wasAdded 
          ? `Reacted with ${reactionEmojis[reactionType]}!` 
          : 'Reaction removed'
        );
      } else {
        toast.error('Failed to react to comment');
      }
    } catch (error) {
      toast.error('Error reacting to comment');
    }
  }, [session, caseId]);

  const updateCommentReactions = (comment: Comment, reactionType: string, isAdding: boolean): Comment => {
    // Safely handle undefined reactions
    const reactions = comment.reactions ?? [];
    const existingReaction = reactions.find(r => 
      r.reactionType === reactionType && r.user.id === session?.user?.id
    );

    let newReactions = [...reactions];
    
    if (isAdding && !existingReaction) {
      // Add new reaction
      newReactions.push({
        id: `temp-${Date.now()}`,
        reactionType,
        user: {
          id: session?.user?.id || '',
          name: session?.user?.name || ''
        }
      });
    } else if (!isAdding && existingReaction) {
      // Remove reaction
      newReactions = newReactions.filter(r => 
        !(r.reactionType === reactionType && r.user.id === session?.user?.id)
      );
    }

    return {
      ...comment,
      reactions: newReactions,
      _count: {
        ...comment._count,
        reactions: newReactions.length
      }
    };
  };

  const getReactionData = (comment: Comment) => {
    const reactionCounts: { [key: string]: number } = {};
    const userReactions: { [key: string]: boolean } = {};

    // Safely handle undefined reactions
    (comment.reactions ?? []).forEach(reaction => {
      reactionCounts[reaction.reactionType] = (reactionCounts[reaction.reactionType] || 0) + 1;
      if (reaction.user.id === session?.user?.id) {
        userReactions[reaction.reactionType] = true;
      }
    });

    return Object.keys(reactionCounts).map(type => ({
      reactionType: type,
      count: reactionCounts[type],
      userReacted: userReactions[type] || false
    }));
  };

  const toggleReplies = (commentId: string) => {
    setShowReplies(prev => {
      const newSet = new Set(prev);
      if (newSet.has(commentId)) {
        newSet.delete(commentId);
      } else {
        newSet.add(commentId);
      }
      return newSet;
    });
  };

  // Memoized CommentItem to prevent unnecessary re-renders
  const CommentItem: React.FC<{ 
    comment: Comment; 
    isReply?: boolean; 
    depth?: number;
    parentId?: string;
  }> = memo(({ comment, isReply = false, depth = 0, parentId }) => {
    const isLiked = likedComments.has(comment.id);
    const showReplyInput = replyingTo === comment.id;
    const isOwner = comment.user.id === session?.user?.id;

    // Memoized event handlers to prevent re-renders
    const handleReplyClick = useCallback(() => {
      setReplyingTo(comment.id);
    }, [comment.id]);

    const handleCancelReply = useCallback(() => {
      setReplyingTo(null);
      setReplyContent(prev => ({ ...prev, [comment.id]: '' }));
    }, [comment.id]);

    const handleAddReplyClick = useCallback(() => {
      handleAddReply(comment.id);
    }, [comment.id]);

    const handleToggleReplies = useCallback(() => {
      toggleReplies(comment.id);
    }, [comment.id]);

    const handleReactionClick = useCallback((reactionType: string) => {
      handleReaction(comment.id, reactionType);
    }, [comment.id]);

    const handleDeleteComment = useCallback(async () => {
      if (!confirm('Are you sure you want to delete this comment? This action cannot be undone.')) {
        return;
      }

      try {
        console.log('Attempting to delete comment:', comment.id, 'from case:', caseId);
        
        const response = await fetch(`/api/live-cases/${caseId}/comments?commentId=${comment.id}`, {
          method: 'DELETE',
        });

        console.log('Delete response status:', response.status);
        
        if (response.ok) {
          const result = await response.json();
          console.log('Delete successful:', result);
          
          // Remove the comment from state
          setComments(prev => {
            if (parentId) {
              // Remove reply from parent comment
              return prev.map(c => {
                if (c.id === parentId) {
                  return {
                    ...c,
                    replies: c.replies.filter(r => r.id !== comment.id),
                    _count: {
                      ...c._count,
                      replies: c._count.replies - 1
                    }
                  };
                }
                return c;
              });
            } else {
              // Remove top-level comment
              return prev.filter(c => c.id !== comment.id);
            }
          });
          toast.success('Comment deleted successfully');
        } else {
          const errorData = await response.json().catch(() => ({}));
          console.error('Delete failed:', response.status, errorData);
          toast.error(`Failed to delete comment: ${errorData.message || response.statusText}`);
        }
      } catch (error) {
        console.error('Error deleting comment:', error);
        toast.error('Error deleting comment');
      }
    }, [comment.id, parentId, caseId]);

    return (
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        className={`${isReply ? 'ml-8 border-l-2 border-gray-100 pl-4' : ''}`}
      >
        <div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
          <div className="flex items-start gap-3">
            {/* User Avatar */}
            <div className="flex-shrink-0">
              <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
                {comment.user.profilePicture ? (
                  <img 
                    src={comment.user.profilePicture} 
                    alt={comment.user.name}
                    className="w-10 h-10 rounded-full object-cover"
                  />
                ) : (
                  <User className="h-5 w-5" />
                )}
              </div>
            </div>

            {/* Comment Content */}
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2 mb-2">
                <span className="font-semibold text-gray-900">{comment.user.name}</span>
                <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
                  {comment.user.role}
                </span>
                <span className="text-xs text-gray-500 flex items-center gap-1">
                  <Clock className="h-3 w-3" />
                  {format(new Date(comment.createdAt), 'MMM d, yyyy • h:mm a')}
                </span>
                {comment.isEdited && (
                  <span className="text-xs text-gray-400">(edited)</span>
                )}
              </div>

              <div className="text-gray-700 mb-3">
                {comment.isDeleted ? (
                  <em className="text-gray-400">This comment has been deleted</em>
                ) : (
                  <p className="whitespace-pre-wrap">{comment.content}</p>
                )}
              </div>

              {/* Comment Actions */}
              {!comment.isDeleted && (
                <div className="flex items-center gap-4 text-sm">
                  <ReactionPicker
                    onReaction={handleReactionClick}
                    currentReactions={getReactionData(comment)}
                    commentId={comment.id}
                  />

                  {!isReply && (
                    <button
                      type="button"
                      onClick={handleReplyClick}
                      className="flex items-center gap-1 px-3 py-1 rounded-full text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors"
                    >
                      <Reply className="h-4 w-4" />
                      <span>Reply</span>
                    </button>
                  )}

                  {(comment._count?.replies || 0) > 0 && !isReply && (
                    <button
                      type="button"
                      onClick={handleToggleReplies}
                      className="flex items-center gap-1 px-3 py-1 rounded-full text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors"
                    >
                      <MessageSquare className="h-4 w-4" />
                      <span>
                        {showReplies.has(comment.id) ? 'Hide' : 'Show'} {comment._count?.replies || 0} replies
                      </span>
                    </button>
                  )}

                  {/* Delete button for comment owner */}
                  {isOwner && (
                    <button
                      type="button"
                      onClick={handleDeleteComment}
                      className="flex items-center gap-1 px-3 py-1 rounded-full text-red-500 hover:text-red-700 hover:bg-red-50 transition-colors"
                    >
                      <Trash2 className="h-4 w-4" />
                      <span>Delete</span>
                    </button>
                  )}
                </div>
              )}
            </div>
          </div>

          {/* Reply Input */}
          {showReplyInput && (
            <motion.div
              initial={{ opacity: 0, height: 0 }}
              animate={{ opacity: 1, height: 'auto' }}
              exit={{ opacity: 0, height: 0 }}
              className="mt-4 ml-12"
            >
              <textarea
                value={replyContent[comment.id] || ''}
                onChange={(e) => {
                  const value = e.target.value;
                  setReplyContent(prev => ({ ...prev, [comment.id]: value }));
                }}
                placeholder="Write a reply..."
                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
                rows={3}
              />
              <div className="flex justify-end gap-2 mt-2">
                <button
                  type="button"
                  onClick={handleCancelReply}
                  className="px-3 py-1 text-gray-600 hover:text-gray-800 transition-colors"
                >
                  Cancel
                </button>
                <button
                  type="button"
                  onClick={handleAddReplyClick}
                  className="px-4 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
                >
                  Reply
                </button>
              </div>
            </motion.div>
          )}

          {/* Replies */}
          {showReplies.has(comment.id) && comment.replies.length > 0 && (
            <div className="mt-4 space-y-3">
              {comment.replies.map((reply) => (
                <CommentItem
                  key={reply.id}
                  comment={reply}
                  isReply={true}
                  depth={depth + 1}
                  parentId={comment.id}
                />
              ))}
            </div>
          )}
        </div>
      </motion.div>
    );
  });

  return (
    <div className="space-y-6">
      {/* Comment Input */}
      <div className="bg-white rounded-lg border border-gray-200 p-6">
        <h3 className="text-lg font-semibold text-gray-900 mb-4">
          Join the Discussion
        </h3>
        
        <div className="flex items-start gap-3">
          <div className="flex-shrink-0">
            <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
              {session?.user?.image ? (
                <img 
                  src={session.user.image} 
                  alt={session.user.name || 'User'}
                  className="w-10 h-10 rounded-full object-cover"
                />
              ) : (
                <User className="h-5 w-5" />
              )}
            </div>
          </div>
          
          <div className="flex-1">
            <textarea
              value={newComment}
              onChange={(e) => setNewComment(e.target.value)}
              placeholder="Share your thoughts on this case..."
              className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
              rows={4}
            />
            <div className="flex justify-between items-center mt-3">
              <span className="text-sm text-gray-500">
                {newComment.length}/1000 characters
              </span>
              <button
                type="button"
                onClick={handleAddComment}
                disabled={!newComment.trim() || posting}
                className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
              >
                {posting ? 'Posting...' : 'Post Comment'}
              </button>
            </div>
          </div>
        </div>
      </div>

      {/* Comments List */}
      <div className="space-y-4">
        <h3 className="text-lg font-semibold text-gray-900">
          Comments ({comments.length})
        </h3>
        
        {loading ? (
          <div className="text-center py-8">
            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
            <p className="text-gray-500 mt-2">Loading comments...</p>
          </div>
        ) : comments.length === 0 ? (
          <div className="text-center py-8 bg-gray-50 rounded-lg">
            <MessageSquare className="h-12 w-12 text-gray-400 mx-auto mb-3" />
            <p className="text-gray-500">No comments yet. Be the first to share your thoughts!</p>
          </div>
        ) : (
          <div className="space-y-4">
            {comments.map((comment) => (
              <CommentItem key={comment.id} comment={comment} />
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default ThreadedComments; 

CasperSecurity Mini