diff --git a/src/components/BlogPostInteractions.tsx b/src/components/BlogPostInteractions.tsx index a42fd4d..6315c58 100644 --- a/src/components/BlogPostInteractions.tsx +++ b/src/components/BlogPostInteractions.tsx @@ -1,15 +1,36 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { Loader2, MessageSquare, Reply, Send, Trash2 } from "lucide-react"; +import { + Check, + Edit3, + Eye, + EyeOff, + Loader2, + MessageSquare, + Reply, + Send, + Trash2, + X, +} from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { api } from "@/lib/api"; import type * as Types from "@/lib/types"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; import { Link } from "@/lib/router"; -import { formatJalaliDate, resolveErrorMessage, toPersianDigits } from "@/lib/utils"; +import { cn, formatJalaliDate, resolveErrorMessage, toPersianDigits } from "@/lib/utils"; import { useToast } from "@/hooks/use-toast"; type Props = { @@ -25,11 +46,15 @@ function avatarInitial(author: Types.CommentSchema["author"]) { return displayName(author).trim()[0] || "ک"; } -function countComments(comments: Types.CommentSchema[]) { - return comments.reduce( - (total, comment) => total + 1 + countComments(comment.replies || []), - 0, - ); +function visibleCommentCount(comments: Types.CommentSchema[]) { + return comments.reduce((total, comment) => { + if (comment.is_deleted || comment.is_hidden) return total; + return total + 1 + visibleCommentCount(comment.replies || []); + }, 0); +} + +function allDescendants(comment: Types.CommentSchema): Types.CommentSchema[] { + return (comment.replies || []).flatMap((reply) => [reply, ...allDescendants(reply)]); } function findComment(comments: Types.CommentSchema[], id: number | null): Types.CommentSchema | undefined { @@ -52,8 +77,14 @@ export default function BlogPostInteractions({ const [commentCount, setCommentCount] = useState(initialComments); const [content, setContent] = useState(""); const [replyTo, setReplyTo] = useState(null); + const [editingId, setEditingId] = useState(null); + const [editContent, setEditContent] = useState(""); + const [expandedReplies, setExpandedReplies] = useState>(new Set()); + const [deleteTarget, setDeleteTarget] = useState(null); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); + const [savingEdit, setSavingEdit] = useState(false); + const [moderatingId, setModeratingId] = useState(null); const canModerate = Boolean(user?.is_staff || user?.is_superuser || user?.can_review_blog_posts); @@ -65,7 +96,7 @@ export default function BlogPostInteractions({ const loadComments = async () => { const data = await api.getComments(slug); setComments(data); - setCommentCount(countComments(data)); + setCommentCount(visibleCommentCount(data)); }; useEffect(() => { @@ -74,7 +105,7 @@ export default function BlogPostInteractions({ .then((data) => { if (!mounted) return; setComments(data); - setCommentCount(countComments(data)); + setCommentCount(visibleCommentCount(data)); }) .finally(() => { if (mounted) setLoading(false); @@ -107,125 +138,331 @@ export default function BlogPostInteractions({ const hideComment = async (commentId: number) => { try { + setModeratingId(commentId); await api.hideComment(commentId); await loadComments(); toast({ title: "کامنت مخفی شد", variant: "success" }); + } catch (error) { + toast({ + title: "مخفی کردن کامنت ناموفق بود", + description: resolveErrorMessage(error, "دوباره تلاش کنید"), + variant: "destructive", + }); + } finally { + setModeratingId(null); + } + }; + + const unhideComment = async (commentId: number) => { + try { + setModeratingId(commentId); + await api.unhideComment(commentId); + await loadComments(); + toast({ title: "کامنت دوباره نمایش داده شد", variant: "success" }); + } catch (error) { + toast({ + title: "نمایش دوباره کامنت ناموفق بود", + description: resolveErrorMessage(error, "دوباره تلاش کنید"), + variant: "destructive", + }); + } finally { + setModeratingId(null); + } + }; + + const deleteComment = async () => { + if (!deleteTarget) return; + try { + setModeratingId(deleteTarget.id); + await api.deleteComment(deleteTarget.id); + setDeleteTarget(null); + await loadComments(); + toast({ title: "کامنت حذف شد", variant: "success" }); } catch (error) { toast({ title: "حذف کامنت ناموفق بود", description: resolveErrorMessage(error, "دوباره تلاش کنید"), variant: "destructive", }); + } finally { + setModeratingId(null); } }; - const renderComment = (comment: Types.CommentSchema, nested = false) => ( -
-
-
- {avatarInitial(comment.author)} -
-
-
- {displayName(comment.author)} - {formatJalaliDate(comment.created_at)} -
-
-

{comment.content}

-
-
- {isAuthenticated ? ( - - ) : null} - {canModerate ? ( - - ) : null} -
-
-
- {comment.replies?.length ? ( -
- {comment.replies.map((reply) => renderComment(reply, true))} -
- ) : null} -
- ); + const startEdit = (comment: Types.CommentSchema) => { + setEditingId(comment.id); + setEditContent(comment.content); + setReplyTo(null); + }; - return ( - - - - کامنت‌ها - - - - {toPersianDigits(commentCount)} کامنت برای این نوشته ثبت شده است. - - - - {!isAuthenticated ? ( -
- برای ثبت کامنت باید وارد حساب کاربری شوید. - + const cancelEdit = () => { + setEditingId(null); + setEditContent(""); + }; + + const saveEdit = async () => { + const trimmed = editContent.trim(); + if (!editingId || !trimmed) return; + try { + setSavingEdit(true); + await api.updateComment(editingId, { content: trimmed }); + await loadComments(); + cancelEdit(); + toast({ title: "کامنت ویرایش شد", variant: "success" }); + } catch (error) { + toast({ + title: "ویرایش کامنت ناموفق بود", + description: resolveErrorMessage(error, "دوباره تلاش کنید"), + variant: "destructive", + }); + } finally { + setSavingEdit(false); + } + }; + + const toggleExpandedReplies = (commentId: number) => { + setExpandedReplies((current) => { + const next = new Set(current); + if (next.has(commentId)) { + next.delete(commentId); + } else { + next.add(commentId); + } + return next; + }); + }; + + const renderComment = (comment: Types.CommentSchema, nested = false, compact = false, parentHidden = false) => { + const isOwnComment = Boolean(user?.id === comment.author.id); + const isEditing = editingId === comment.id; + const hidden = parentHidden || Boolean(comment.is_hidden); + const deeperReplies = nested ? allDescendants(comment) : []; + const showDeeperReplies = expandedReplies.has(comment.id); + + return ( +
+