diff --git a/client/components/feed/CommentsModal.tsx b/client/components/feed/CommentsModal.tsx new file mode 100644 index 00000000..4863029a --- /dev/null +++ b/client/components/feed/CommentsModal.tsx @@ -0,0 +1,308 @@ +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { aethexToast } from "@/lib/aethex-toast"; +import { MessageCircle, Send, Trash2, Loader2 } from "lucide-react"; + +const API_BASE = import.meta.env.VITE_API_BASE || ""; + +interface Comment { + id: string; + content: string; + created_at: string; + user_id: string; + user_profiles?: { + id: string; + username?: string; + full_name?: string; + avatar_url?: string; + }; +} + +interface CommentsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + postId: string; + currentUserId?: string; + onCommentAdded?: () => void; +} + +export default function CommentsModal({ + open, + onOpenChange, + postId, + currentUserId, + onCommentAdded, +}: CommentsModalProps) { + const [comments, setComments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [newComment, setNewComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Load comments when modal opens + useEffect(() => { + if (open) { + loadComments(); + } + }, [open]); + + const loadComments = async () => { + setIsLoading(true); + try { + const response = await fetch( + `${API_BASE}/api/community/post-comments?post_id=${postId}&limit=50` + ); + if (response.ok) { + const data = await response.json(); + setComments(data.comments || []); + } else { + aethexToast.error({ + title: "Failed to load comments", + description: "Please try again", + }); + } + } catch (error) { + console.error("Failed to load comments:", error); + aethexToast.error({ + title: "Failed to load comments", + description: "An unexpected error occurred", + }); + } finally { + setIsLoading(false); + } + }; + + const handleAddComment = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!currentUserId) { + aethexToast.error({ + title: "Not authenticated", + description: "Please log in to comment", + }); + return; + } + + if (newComment.trim().length === 0) { + aethexToast.error({ + title: "Empty comment", + description: "Please write something", + }); + return; + } + + setIsSubmitting(true); + try { + const response = await fetch(`${API_BASE}/api/community/post-comments`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + post_id: postId, + user_id: currentUserId, + content: newComment, + }), + }); + + if (response.ok) { + const data = await response.json(); + setComments((prev) => [data.comment, ...prev]); + setNewComment(""); + aethexToast.success({ + title: "Comment added", + description: "Your comment has been posted", + }); + onCommentAdded?.(); + } else { + const error = await response.json(); + aethexToast.error({ + title: "Failed to add comment", + description: error.error || "Please try again", + }); + } + } catch (error) { + console.error("Failed to add comment:", error); + aethexToast.error({ + title: "Failed to add comment", + description: "An unexpected error occurred", + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleDeleteComment = async (commentId: string) => { + setIsSubmitting(true); + try { + const response = await fetch( + `${API_BASE}/api/community/post-comments`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + comment_id: commentId, + user_id: currentUserId, + }), + } + ); + + if (response.ok) { + setComments((prev) => + prev.filter((comment) => comment.id !== commentId) + ); + aethexToast.success({ + title: "Comment deleted", + description: "Your comment has been removed", + }); + onCommentAdded?.(); + } else { + const error = await response.json(); + aethexToast.error({ + title: "Failed to delete comment", + description: error.error || "Please try again", + }); + } + } catch (error) { + console.error("Failed to delete comment:", error); + aethexToast.error({ + title: "Failed to delete comment", + description: "An unexpected error occurred", + }); + } finally { + setIsSubmitting(false); + } + }; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSecs = Math.floor(diffMs / 1000); + const diffMins = Math.floor(diffSecs / 60); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffSecs < 60) return "just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + }; + + return ( + + + + + + Comments ({comments.length}) + + + + + + + {isLoading ? ( +
+ +
+ ) : comments.length === 0 ? ( +
+ +

+ No comments yet. Be the first to comment! +

+
+ ) : ( +
+ {comments.map((comment) => ( +
+
+
+ {comment.user_profiles?.avatar_url && ( + {comment.user_profiles.full_name + )} +
+

+ {comment.user_profiles?.full_name || + comment.user_profiles?.username || + "Anonymous"} +

+

+ {formatDate(comment.created_at)} +

+

+ {comment.content} +

+
+
+ {currentUserId === comment.user_id && ( + + )} +
+
+ ))} +
+ )} +
+ + + +
+