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/SimpleComments.tsx
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; 

CasperSecurity Mini