import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { formatDistanceToNow } from 'date-fns'; import { fr } from 'date-fns/locale'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'; import { MessageCircle, Reply, Trash2, Edit2, MoreVertical, Send, X, Loader2, } from 'lucide-react'; import { useAuthStore } from '@/features/auth/store/authStore'; import { useToast } from '@/hooks/useToast'; import { createComment, updateComment, deleteComment, getReplies, type TrackComment, } from '../services/commentService'; import { cn } from '@/lib/utils'; /** * FE-COMP-012: Comment thread component with replies and moderation */ interface CommentThreadProps { comment: TrackComment; trackId: string; depth?: number; className?: string; } const MAX_DEPTH = 3; // Maximum nesting depth for replies export function CommentThread({ comment, trackId, depth = 0, className, }: CommentThreadProps) { const { user } = useAuthStore(); const { success: showSuccess, error: showError } = useToast(); const queryClient = useQueryClient(); const [isReplying, setIsReplying] = useState(false); const [isEditing, setIsEditing] = useState(false); const [replyContent, setReplyContent] = useState(''); const [editContent, setEditContent] = useState(comment.content); const [showReplies, setShowReplies] = useState(depth === 0); const [showDeleteDialog, setShowDeleteDialog] = useState(false); // Fetch replies const { data: repliesData, isLoading: isLoadingReplies, } = useQuery({ queryKey: ['commentReplies', comment.id], queryFn: () => getReplies(comment.id, 1, 20), enabled: showReplies && !comment.replies, }); const replies = comment.replies || repliesData?.replies || []; const canReply = depth < MAX_DEPTH; const canEdit = user?.id === comment.user_id; const canDelete = user?.id === comment.user_id || user?.role === 'admin'; // Create reply mutation const createReplyMutation = useMutation({ mutationFn: (content: string) => createComment(trackId, content, comment.id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['trackComments', trackId] }); queryClient.invalidateQueries({ queryKey: ['commentReplies', comment.id] }); setReplyContent(''); setIsReplying(false); setShowReplies(true); showSuccess('Réponse publiée'); }, onError: (error: any) => { showError(error.message || 'Erreur lors de la publication de la réponse'); }, }); // Update comment mutation const updateCommentMutation = useMutation({ mutationFn: (content: string) => updateComment(comment.id, content), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['trackComments', trackId] }); setIsEditing(false); showSuccess('Commentaire modifié'); }, onError: (error: any) => { showError(error.message || 'Erreur lors de la modification'); }, }); // Delete comment mutation const deleteCommentMutation = useMutation({ mutationFn: () => deleteComment(comment.id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['trackComments', trackId] }); setShowDeleteDialog(false); showSuccess('Commentaire supprimé'); }, onError: (error: any) => { showError(error.message || 'Erreur lors de la suppression'); }, }); const handleReplySubmit = (e: React.FormEvent) => { e.preventDefault(); if (!replyContent.trim() || !user) return; createReplyMutation.mutate(replyContent.trim()); }; const handleEditSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!editContent.trim()) return; updateCommentMutation.mutate(editContent.trim()); }; const handleDelete = () => { deleteCommentMutation.mutate(); }; return ( <>
{/* Main Comment */}
{comment.user?.username?.charAt(0).toUpperCase() || 'U'}
{/* Comment Header */}
{comment.user?.username || 'Utilisateur'} {formatDistanceToNow(new Date(comment.created_at), { addSuffix: true, locale: fr, })} {comment.is_edited && ( (modifié) )}
{(canEdit || canDelete) && ( {canEdit && ( setIsEditing(true)}> Modifier )} {canDelete && ( setShowDeleteDialog(true)} className="text-destructive" > Supprimer )} )}
{/* Comment Content */} {isEditing ? (
setEditContent(e.target.value)} maxLength={500} autoFocus />
) : (

{comment.content}

)} {/* Comment Actions */} {!isEditing && (
{canReply && user && ( )} {replies.length > 0 && ( )}
)} {/* Reply Form */} {isReplying && user && (
setReplyContent(e.target.value)} placeholder={`Répondre à ${comment.user?.username}...`} maxLength={500} autoFocus />
)} {/* Replies */} {showReplies && (
{isLoadingReplies ? (
) : replies.length > 0 ? ( replies.map((reply) => ( )) ) : null}
)}
{/* Delete Confirmation Dialog */} setShowDeleteDialog(false)} onConfirm={handleDelete} title="Supprimer le commentaire" description="Êtes-vous sûr de vouloir supprimer ce commentaire ? Cette action est irréversible." confirmLabel="Supprimer" cancelLabel="Annuler" variant="destructive" /> ); }