import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { ThumbsUp, ThumbsDown, ChevronDown, ChevronUp, ArrowUpDown } from "lucide-react";
import { formatTimeAgo } from "@/lib/utils";
import { apiRequest } from "@/lib/queryClient";
import AvatarPreviewDialog from "@/components/AvatarPreviewDialog";
interface Comment {
id: string;
content: string;
nickname: string;
avatar?: string;
createdAt: string;
likesCount: number;
dislikesCount: number;
repliesCount: number;
parentId?: string;
}
interface CommentSectionProps {
articleId: string;
}
function getUserIdentifier() {
let userId = localStorage.getItem('commentUserId');
if (!userId) {
userId = 'user_' + Math.random().toString(36).substring(2, 15);
localStorage.setItem('commentUserId', userId);
}
return userId;
}
function CommentForm({
articleId,
parentId,
onSuccess,
placeholder = "Add comment...",
buttonText = "Comment"
}: {
articleId: string;
parentId?: string;
onSuccess?: () => void;
placeholder?: string;
buttonText?: string;
}) {
const [content, setContent] = useState("");
const [nickname, setNickname] = useState(() =>
localStorage.getItem('commentNickname') || ""
);
const [showForm, setShowForm] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const queryClient = useQueryClient();
const createComment = useMutation({
mutationFn: async (data: { content: string; nickname: string; parentId?: string }) => {
const response = await apiRequest('POST', `/api/articles/${articleId}/comments`, data);
return response.json();
},
onSuccess: () => {
setContent("");
setShowForm(false);
setIsFocused(false);
localStorage.setItem('commentNickname', nickname);
queryClient.invalidateQueries({ queryKey: ['/api/articles', articleId, 'comments'] });
if (parentId) {
queryClient.invalidateQueries({ queryKey: ['/api/comments', parentId, 'replies'] });
}
onSuccess?.();
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim() || !nickname.trim()) return;
createComment.mutate({
content: content.trim(),
nickname: nickname.trim(),
parentId
});
};
if (!showForm && parentId) {
return (
);
}
if (!showForm && !parentId) {
return (
setShowForm(true)}
data-testid="button-add-comment"
>
👤
);
}
return (
);
}
function CommentItem({ comment, articleId, level = 0 }: {
comment: Comment;
articleId: string;
level?: number;
}) {
const [showReplies, setShowReplies] = useState(false);
const [showReplyForm, setShowReplyForm] = useState(false);
const [userReaction, setUserReaction] = useState<'like' | 'dislike' | null>(null);
const queryClient = useQueryClient();
const userIdentifier = getUserIdentifier();
const { data: reactionData } = useQuery({
queryKey: ['/api/comments', comment.id, 'reactions', userIdentifier],
queryFn: async () => {
const response = await apiRequest('GET', `/api/comments/${comment.id}/reactions/${userIdentifier}`);
return response.json();
},
});
const { data: repliesData } = useQuery({
queryKey: ['/api/comments', comment.id, 'replies'],
queryFn: async () => {
const response = await apiRequest('GET', `/api/comments/${comment.id}/replies`);
return response.json();
},
enabled: showReplies && comment.repliesCount > 0,
});
const toggleReaction = useMutation({
mutationFn: async (reactionType: 'like' | 'dislike') => {
const response = await apiRequest('POST', `/api/comments/${comment.id}/reactions`, {
reactionType,
userIdentifier
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/articles', articleId, 'comments'] });
queryClient.invalidateQueries({ queryKey: ['/api/comments', comment.id, 'reactions', userIdentifier] });
},
});
const currentReaction = reactionData?.reactionType || userReaction;
const handleReaction = (reactionType: 'like' | 'dislike') => {
setUserReaction(currentReaction === reactionType ? null : reactionType);
toggleReaction.mutate(reactionType);
};
return (
0 ? 'ml-10 pl-6 border-l border-muted' : ''} py-3`}>
{comment.avatar ? (
{comment.nickname.charAt(0).toUpperCase()}
) : (
👤
)}
{comment.nickname}
{formatTimeAgo(comment.createdAt)}
{comment.content}
{level < 2 && (
setShowReplyForm(false)}
/>
)}
{comment.repliesCount > 0 && (
)}
{showReplies && repliesData?.comments && (
{repliesData.comments.map((reply: Comment) => (
))}
)}
);
}
export default function CommentSection({ articleId }: CommentSectionProps) {
const [sortBy, setSortBy] = useState<'newest' | 'popular'>('newest');
const { data, isLoading } = useQuery({
queryKey: ['/api/articles', articleId, 'comments', sortBy],
queryFn: async () => {
const response = await apiRequest('GET', `/api/articles/${articleId}/comments?limit=20&sort=${sortBy}`);
return response.json();
},
});
if (isLoading) {
return (
Comments
{[...Array(3)].map((_, i) => (
))}
);
}
const comments = data?.comments || [];
const total = data?.total || 0;
return (
{/* Comments Header */}
{total} Comments
{/* Comment Form */}
{/* Comments List */}
{comments.length === 0 ? (
💬
No comments yet
Be the first to leave a comment!
) : (
{comments.map((comment: Comment, index: number) => (
))}
)}
);
}