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/CommentsSection.tsx
import React, { useState, useEffect, useRef } from 'react';
import {
  Box,
  Flex,
  Badge,
  Button,
  Textarea,
  Text,
  Input,
  Spinner,
  Skeleton,
  Alert,
} from '@chakra-ui/react';
import { FaReply, FaEdit, FaTrash, FaThumbsUp, FaFlag, FaPaperclip, FaSync } from 'react-icons/fa';
import { useSession } from 'next-auth/react';
import { useWebSocket } from '../context/StableWebSocketContext';

const roleColors: Record<string, string> = {
  SUPERADMIN: 'purple',
  MODERATOR: 'blue',
  USER: 'gray',
};

const ALLOWED_TYPES = [
  'image/jpeg', 'image/png', 'image/gif', 'image/webp',
  'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain'
];
const MAX_FILES = 3;
const MAX_SIZE = 5 * 1024 * 1024;

function CommentCard({ comment, isReply = false, onReply, replyingId, onSubmitReply, replyValue, onReplyChange, isReplying, isLoggedIn, postingReply, onDelete, deletingId, onLike, likingId, currentUser, replyFileInputRef, replyFiles, replyFileError, handleReplyFileChange, handleRemoveReplyFile }: any) {
  const isAuthor = currentUser && comment.user?.id === currentUser.id;
  const isAdmin = currentUser && (currentUser.role === 'SUPERADMIN' || currentUser.role === 'ADMIN');
  const canDelete = isAuthor || isAdmin;
  const hasLiked = comment.reactions?.some((r: any) => r.user?.id === currentUser?.id);
  const isDeleting = deletingId === comment.id;
  const isLiking = likingId === comment.id;
  return (
    <Box
      bg="white"
      borderRadius="lg"
      boxShadow="md"
      p={4}
      mt={isReply ? 2 : 4}
      ml={isReply ? 8 : 0}
      w="full"
    >
      <Flex align="center" mb={2}>
        <Box mr={3} w="32px" h="32px" borderRadius="full" bg="gray.200" display="flex" alignItems="center" justifyContent="center" overflow="hidden">
          {comment.user?.profilePicture ? (
            <img src={comment.user.profilePicture} alt={comment.user.name} style={{ width: 32, height: 32, objectFit: 'cover' }} />
          ) : (
            <Text fontSize="md" fontWeight="bold" color="gray.600">{comment.user?.name?.split(' ').map((n: string) => n[0]).join('').slice(0,2) || '?'}</Text>
          )}
        </Box>
        <Text fontWeight="bold" mr={2}>{comment.user?.name}</Text>
        {comment.user?.role && (
          <Badge colorScheme={roleColors[comment.user.role] || 'gray'} mr={2}>
            {comment.user.role}
          </Badge>
        )}
      </Flex>
      <Text mb={2}>{comment.content}</Text>
      {/* Attachments display */}
      {comment.attachments && comment.attachments.length > 0 && (
        <Box mt={2}>
          <Text fontSize="xs" color="gray.500" mb={1}>Attachments:</Text>
          <Flex gap={2} wrap="wrap">
            {comment.attachments.map((att: any) => (
              <Box key={att.id} p={1} borderWidth={1} borderRadius="md" bg="gray.50">
                {att.type.startsWith('image/') ? (
                  <a href={att.url} target="_blank" rel="noopener noreferrer">
                    <img src={att.url} alt={att.name} style={{ maxWidth: 80, maxHeight: 80, borderRadius: 4 }} />
                  </a>
                ) : (
                  <a href={att.url} target="_blank" rel="noopener noreferrer">
                    <Text fontSize="xs">{att.name}</Text>
                  </a>
                )}
              </Box>
            ))}
          </Flex>
        </Box>
      )}
      <Flex gap="8px" align="center" mt={3}>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => onReply(comment.id)}
          disabled={!isLoggedIn}
        >
          <FaReply style={{marginRight: 8}} />
          Reply
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => onLike(comment.id)}
          disabled={!isLoggedIn || isLiking}
        >
          {isLiking ? <Spinner size="xs" /> : <FaThumbsUp style={{marginRight: 8}} />}
          {comment.likes || 0}
        </Button>
        {canDelete && (
          <Button
            size="sm"
            variant="ghost"
            onClick={() => onDelete(comment.id)}
            disabled={isDeleting}
          >
            {isDeleting ? <Spinner size="xs" /> : <FaTrash style={{marginRight: 8}} />}
            Delete
          </Button>
        )}
        <Text fontSize="xs" color="gray.400" ml="auto">
          {new Date(comment.createdAt).toLocaleDateString()} at {new Date(comment.createdAt).toLocaleTimeString()}
        </Text>
      </Flex>
      {/* Reply box */}
      {replyingId === comment.id && (
        <Box mt={3} w="full">
          <Textarea
            placeholder="Write a reply... (Ctrl+Enter to submit)"
            value={replyValue}
            onChange={e => onReplyChange(e.target.value)}
            minH="40px"
            maxLength={1000}
            disabled={postingReply}
            onKeyDown={e => {
              if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
                onSubmitReply(comment.id);
              }
            }}
          />
          {/* Reply file picker and preview */}
          <Flex align="center" mt={2} mb={2} gap={2}>
            <Button
              size="sm"
              variant="ghost"
              onClick={() => replyFileInputRef.current?.click()}
              disabled={replyFiles.length >= MAX_FILES || postingReply}
            >
              <FaPaperclip style={{marginRight: 8}} />
              Attach files
            </Button>
            <input
              type="file"
              multiple
              accept={ALLOWED_TYPES.join(',')}
              style={{ display: 'none' }}
              ref={replyFileInputRef}
              onChange={handleReplyFileChange}
              disabled={postingReply}
            />
            <Text fontSize="xs" color="gray.500">Max 5MB per file, {MAX_FILES} files</Text>
          </Flex>
          {replyFileError && <Text color="red.500" fontSize="sm">{replyFileError}</Text>}
          <Flex gap={2} wrap="wrap" mb={2}>
            {replyFiles.map((file: File, idx: number) => (
              <Box key={idx} p={1} borderWidth={1} borderRadius="md" bg="gray.50">
                <Text fontSize="xs">{file.name}</Text>
                <Button size="xs" colorScheme="red" variant="ghost" onClick={() => handleRemoveReplyFile(idx)}>Remove</Button>
              </Box>
            ))}
          </Flex>
          <Flex mt={2} justify="flex-end" gap={2}>
            <Button size="sm" onClick={() => onSubmitReply(comment.id)} colorScheme="blue" loading={postingReply} disabled={!replyValue.trim() || postingReply}>
              Reply
            </Button>
            <Button size="sm" variant="ghost" onClick={() => onReply(null)} disabled={postingReply}>Cancel</Button>
          </Flex>
        </Box>
      )}
      {comment.replies && comment.replies.length > 0 && (
        <Flex direction="column" gap="8px" align="start" mt={2}>
          {comment.replies.map((reply: any) => (
            <CommentCard
              key={reply.id}
              comment={reply}
              isReply
              onReply={onReply}
              replyingId={replyingId}
              onSubmitReply={onSubmitReply}
              replyValue={replyValue}
              onReplyChange={onReplyChange}
              isReplying={isReplying}
              isLoggedIn={isLoggedIn}
              postingReply={postingReply}
              onDelete={onDelete}
              deletingId={deletingId}
              onLike={onLike}
              likingId={likingId}
              currentUser={currentUser}
              replyFileInputRef={replyFileInputRef}
              replyFiles={replyFiles}
              replyFileError={replyFileError}
              handleReplyFileChange={handleReplyFileChange}
              handleRemoveReplyFile={handleRemoveReplyFile}
            />
          ))}
        </Flex>
      )}
    </Box>
  );
}

const CommentsSection = ({ caseId, apiEndpoint }: { caseId: string; apiEndpoint?: string }) => {
  const { data: session, status } = useSession();
  const { ws } = useWebSocket();
  const [comment, setComment] = useState('');
  const [comments, setComments] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [posting, setPosting] = useState(false);
  const [postError, setPostError] = useState<string | null>(null);
  const [replyingId, setReplyingId] = useState<string | null>(null);
  const [replyValue, setReplyValue] = useState('');
  const [postingReply, setPostingReply] = useState(false);
  const [replyError, setReplyError] = useState<string | null>(null);
  const [deletingId, setDeletingId] = useState<string | null>(null);
  const [likingId, setLikingId] = useState<string | null>(null);
  const [files, setFiles] = useState<File[]>([]);
  const [fileError, setFileError] = useState<string | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [replyFiles, setReplyFiles] = useState<File[]>([]);
  const [replyFileError, setReplyFileError] = useState<string | null>(null);
  const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
  const [successMessage, setSuccessMessage] = useState<string | null>(null);
  const replyFileInputRef = useRef<HTMLInputElement>(null);

  const fetchComments = () => {
    if (!caseId) return;
    setLoading(true);
    setError(null);
    const endpoint = apiEndpoint || `/api/live-cases/${caseId}/comments
    fetch(endpoint)
      .then(res => {
        if (!res.ok) {
          throw new Error(
        }
        return res.json();
      })
      .then(data => {
        setComments(data.comments || []);
        setLoading(false);
        setLastUpdate(new Date());
      })
      .catch((err) => {
        setError('Failed to load comments. Please try again.');
        setLoading(false);
      });
  };

  useEffect(() => {
    fetchComments();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [caseId]);

  // WebSocket event listeners for real-time updates
  useEffect(() => {
    if (!ws || !caseId) return;

    const handleNewComment = (data: any) => {
      if (data.caseId === caseId) {
        fetchComments();
      }
    };

    const handleCommentUpdate = (data: any) => {
      if (data.caseId === caseId) {
        fetchComments();
      }
    };

    // Native WebSocket event handling
    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        if (data.type === 'comment:new') handleNewComment(data);
        if (data.type === 'comment:updated') handleCommentUpdate(data);
        if (data.type === 'comment:deleted') handleCommentUpdate(data);
      } catch (e) {
        // Ignore invalid JSON
      }
    };

    return () => {
      ws.onmessage = null;
    };
  }, [ws, caseId]);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFileError(null);
    const selected = Array.from(e.target.files || []);
    if (selected.length + files.length > MAX_FILES) {
      setFileError(
      return;
    }
    for (const file of selected) {
      if (!ALLOWED_TYPES.includes(file.type)) {
        setFileError('Unsupported file type.');
        return;
      }
      if (file.size > MAX_SIZE) {
        setFileError('File too large (max 5MB each).');
        return;
      }
    }
    setFiles([...files, ...selected]);
  };

  const handleRemoveFile = (idx: number) => {
    setFiles(files.filter((_, i) => i !== idx));
  };

  const handleReplyFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setReplyFileError(null);
    const selected = Array.from(e.target.files || []);
    if (selected.length + replyFiles.length > MAX_FILES) {
      setReplyFileError(
      return;
    }
    for (const file of selected) {
      if (!ALLOWED_TYPES.includes(file.type)) {
        setReplyFileError('Unsupported file type.');
        return;
      }
      if (file.size > MAX_SIZE) {
        setReplyFileError('File too large (max 5MB each).');
        return;
      }
    }
    setReplyFiles([...replyFiles, ...selected]);
  };

  const handleRemoveReplyFile = (idx: number) => {
    setReplyFiles(replyFiles.filter((_, i) => i !== idx));
  };

  const handlePost = async () => {
    if (!comment.trim()) return;
    setPosting(true);
    setPostError(null);
    try {
      const endpoint = apiEndpoint || `/api/live-cases/${caseId}/comments
      let res;
      if (files.length > 0) {
        const formData = new FormData();
        formData.append('content', comment);
        files.forEach(f => formData.append('attachments', f));
        res = await fetch(endpoint, {
          method: 'POST',
          body: formData,
        });
      } else {
        res = await fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ content: comment }),
        });
      }
      if (res.ok) {
        setComment('');
        setFiles([]);
        setSuccessMessage('Comment posted successfully!');
        setPostError(null);
        setTimeout(() => setSuccessMessage(null), 3000);
        fetchComments();
      } else {
        const data = await res.json();
        const errorMessage = data.message || 'Failed to post comment';
        setPostError(errorMessage);
        setSuccessMessage(null);
      }
    } catch (e) {
      const errorMessage = 'Failed to post comment. Please check your connection.';
      setPostError(errorMessage);
      setSuccessMessage(null);
    }
    setPosting(false);
  };

  const handleReply = (id: string | null) => {
    setReplyingId(id);
    setReplyValue('');
    setReplyError(null);
  };

  const handleSubmitReply = async (parentId: string) => {
    if (!replyValue.trim()) return;
    setPostingReply(true);
    setReplyError(null);
    try {
      const endpoint = apiEndpoint || `/api/live-cases/${caseId}/comments
      let res;
      if (replyFiles.length > 0) {
        const formData = new FormData();
        formData.append('content', replyValue);
        formData.append('parentId', parentId);
        replyFiles.forEach(f => formData.append('attachments', f));
        res = await fetch(endpoint, {
          method: 'POST',
          body: formData,
        });
      } else {
        res = await fetch(endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ content: replyValue, parentId }),
        });
      }
      if (res.ok) {
        setReplyingId(null);
        setReplyValue('');
        setReplyFiles([]);
        fetchComments();
      } else {
        const data = await res.json();
        setReplyError(data.message || 'Failed to post reply');
      }
    } catch (e) {
      setReplyError('Failed to post reply');
    }
    setPostingReply(false);
  };

  const handleDelete = async (commentId: string) => {
    if (!window.confirm('Are you sure you want to delete this comment and all its replies?')) return;
    setDeletingId(commentId);
    try {
      const endpoint = apiEndpoint || `/api/live-cases/${caseId}/comments
      const res = await fetch(`${endpoint}?commentId=${commentId}
        method: 'DELETE',
      });
      if (res.ok) {
        fetchComments();
      } else {
        alert('Failed to delete comment.');
      }
    } catch {
      alert('Failed to delete comment.');
    }
    setDeletingId(null);
  };

  const handleLike = async (commentId: string) => {
    setLikingId(commentId);
    try {
      const endpoint = apiEndpoint || `/api/live-cases/${caseId}/comments
      await fetch(`${endpoint}/${commentId}/like
        method: 'POST',
      });
      fetchComments();
    } catch {}
    setLikingId(null);
  };

  const isLoggedIn = status === 'authenticated';
  const currentUser = session?.user;

  return (
    <Box maxW="2xl" mx="auto" mt={8}>
      {/* Header with refresh button */}
      <Flex justify="space-between" align="center" mb={4}>
        <Text fontSize="lg" fontWeight="semibold">Comments & Discussion</Text>
        <Button
          size="sm"
          variant="ghost"
          onClick={fetchComments}
          disabled={loading}
        >
          <FaSync style={{marginRight: 8}} />
          Refresh
        </Button>
      </Flex>

      {/* Success Alert */}
      {successMessage && (
        <Box bg="green.100" border="1px solid" borderColor="green.300" color="green.800" mb={4} borderRadius="md" p={3}>
          <Text>{successMessage}</Text>
        </Box>
      )}

      {/* Error Alert */}
      {error && (
        <Box bg="red.100" border="1px solid" borderColor="red.300" color="red.800" mb={4} borderRadius="md" p={3}>
          <Box flex="1">
            <Text fontWeight="medium">Failed to load comments</Text>
          </Box>
        </Box>
      )}

      {/* Post Comment Box */}
      <Box
        bg="white"
        borderRadius="lg"
        boxShadow="md"
        p={4}
        mb={6}
      >
        <Flex align="flex-start">
          <Box mr={4} w="40px" h="40px" borderRadius="full" bg="gray.200" display="flex" alignItems="center" justifyContent="center" overflow="hidden">
            {session?.user?.image ? (
              <img src={session.user.image} alt={session.user.name} style={{ width: 40, height: 40, objectFit: 'cover' }} />
            ) : (
              <Text fontSize="lg" fontWeight="bold" color="gray.600">{session?.user?.name?.split(' ').map((n: string) => n[0]).join('').slice(0,2) || '?'}</Text>
            )}
          </Box>
          <Box flex="1">
            <Textarea
              placeholder={isLoggedIn ? "Share your thoughts on this case... (Ctrl+Enter to submit)" : "Please sign in to comment."}
              value={comment}
              onChange={e => setComment(e.target.value)}
              resize="vertical"
              mb={2}
              minH="60px"
              maxLength={1000}
              disabled={!isLoggedIn || posting}
              onKeyDown={e => {
                if (isLoggedIn && (e.ctrlKey || e.metaKey) && e.key === 'Enter') {
                  handlePost();
                }
              }}
            />
            {/* File picker and preview */}
            <Flex align="center" mt={2} mb={2} gap={2}>
              <Button
                size="sm"
                variant="ghost"
                onClick={() => fileInputRef.current?.click()}
                disabled={files.length >= MAX_FILES || posting}
              >
                <FaPaperclip style={{marginRight: 8}} />
                Attach files
              </Button>
              <input
                type="file"
                multiple
                accept={ALLOWED_TYPES.join(',')}
                style={{ display: 'none' }}
                ref={fileInputRef}
                onChange={handleFileChange}
                disabled={posting}
              />
              <Text fontSize="xs" color="gray.500">Max 5MB per file, {MAX_FILES} files</Text>
            </Flex>
            {fileError && <Text color="red.500" fontSize="sm">{fileError}</Text>}
            <Flex gap={2} wrap="wrap" mb={2}>
              {files.map((file, idx) => (
                <Box key={idx} p={1} borderWidth={1} borderRadius="md" bg="gray.50">
                  <Text fontSize="xs">{file.name}</Text>
                  <Button size="xs" colorScheme="red" variant="ghost" onClick={() => handleRemoveFile(idx)}>Remove</Button>
                </Box>
              ))}
            </Flex>
            <Flex justify="space-between" align="center">
              <Button colorScheme="blue" px={6} onClick={handlePost} loading={posting} disabled={!isLoggedIn || !comment.trim() || posting}>
                Post Comment
              </Button>
            </Flex>
            <Text fontSize="xs" color="gray.400" mt={1}>
              {comment.length}/1000 characters
            </Text>
            {postError && <Text color="red.500" fontSize="sm" mt={1}>{postError}</Text>}
            {!isLoggedIn && <Text color="gray.500" fontSize="sm" mt={1}>You must be signed in to post a comment.</Text>}
          </Box>
        </Flex>
      </Box>
      {/* Comments List */}
      {loading ? (
        <Flex direction="column" gap="16px" align="stretch">
          {[...Array(3)].map((_, i) => (
            <Skeleton key={i} height="120px" borderRadius="lg" />
          ))}
        </Flex>
      ) : (
        <Flex direction="column" gap="16px" align="stretch">
          {comments.length === 0 ? (
            <Box textAlign="center" py={8} bg="gray.50" borderRadius="lg">
              <Text color="gray.500" fontSize="lg" mb={2}>No comments yet</Text>
              <Text color="gray.400" fontSize="sm">Be the first to share your thoughts on this case!</Text>
            </Box>
          ) : (
            <>
              <Text fontSize="sm" color="gray.500" mb={2}>
                {comments.length} comment{comments.length !== 1 ? 's' : ''} • Last updated {lastUpdate.toLocaleTimeString()}
              </Text>
              {comments.map(comment => (
              <CommentCard
                key={comment.id}
                comment={comment}
                onReply={handleReply}
                replyingId={replyingId}
                onSubmitReply={handleSubmitReply}
                replyValue={replyValue}
                onReplyChange={setReplyValue}
                isReplying={replyingId === comment.id}
                isLoggedIn={isLoggedIn}
                postingReply={postingReply}
                onDelete={handleDelete}
                deletingId={deletingId}
                onLike={handleLike}
                likingId={likingId}
                currentUser={currentUser}
                replyFileInputRef={replyFileInputRef}
                  replyFiles={replyFiles}
                  replyFileError={replyFileError}
                  handleReplyFileChange={handleReplyFileChange}
                  handleRemoveReplyFile={handleRemoveReplyFile}
              />
              ))}
            </>
          )}
        </Flex>
      )}
      {replyError && (
        <Box bg="red.100" border="1px solid" borderColor="red.300" color="red.800" mt={4} borderRadius="md" p={3}>
          <Text fontSize="sm">{replyError}</Text>
        </Box>
      )}
    </Box>
  );
};

export default CommentsSection; 

CasperSecurity Mini