"use client"; import type { BlogComment } from "@/types/blog"; import { useMemo, useState } from "react"; import CommentForm from "./CommentForm"; import { formatLongDate } from "@/utils"; interface CommentsSectionProps { slug: string; comments: BlogComment[]; } export default function CommentsSection({ slug, comments }: CommentsSectionProps) { const [showAllComments, setShowAllComments] = useState(false); const [expandedCommentKeys, setExpandedCommentKeys] = useState>( () => new Set() ); const [replyTarget, setReplyTarget] = useState<{ // ID của comment gốc để lưu làm parentId (giữ thread 1 cấp trên backend) parentId: string; // Tên tác giả mà chúng ta đang reply (dùng cho UI @mention) replyToName: string; // ID của item trong UI đang hiển thị form reply bên dưới anchorId: string; } | null>(null); // Helper function để lấy chữ cái đầu của tên const getInitials = (name?: string): string => { if (!name) return "?"; const trimmed = name.trim(); if (trimmed.length === 0) return "?"; return trimmed.charAt(0).toUpperCase(); }; const makeCommentKey = (prefix: "p" | "r", id: string | undefined, index: number) => id ? `${prefix}:${id}` : `${prefix}:idx:${index}`; const isExpanded = (key: string) => expandedCommentKeys.has(key); const toggleExpanded = (key: string) => { setExpandedCommentKeys((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }; const { parents, repliesByParent } = useMemo(() => { const parentsLocal = (comments || []).filter((c) => !c.parentId); const replies = (comments || []).filter((c) => !!c.parentId); const map = new Map(); for (const r of replies) { const pid = r.parentId as string; map.set(pid, [...(map.get(pid) || []), r]); } return { parents: parentsLocal, repliesByParent: map }; }, [comments]); // Thu thập tất cả tên tác giả để phát hiện @mention đầy đủ (bao gồm tên nhiều từ) const authorNames = useMemo(() => { return (comments || []) .map((c) => c.authorName) .filter((n): n is string => !!n) // Sắp xếp theo độ dài giảm dần để khớp tên dài nhất trước .sort((a, b) => b.length - a.length); }, [comments]); const renderContentWithMention = (text?: string) => { if (!text) return null; if (!text.startsWith("@")) return text; // Thử khớp @mention với tên tác giả đã biết // Hỗ trợ tên nhiều từ như "@Bạn Cũng Thấy Thế Hà" const matchedName = authorNames.find((name) => { const candidate = `@${name}`; if (!text.startsWith(candidate)) return false; // Đảm bảo khớp kết thúc ở cuối chuỗi hoặc theo sau bởi khoảng trắng const nextChar = text.charAt(candidate.length); return candidate.length === text.length || nextChar === " "; }); if (!matchedName) { // Fallback: trả về text gốc nếu không thể khớp tên một cách chắc chắn return text; } const mention = `@${matchedName}`; const rest = text.slice(mention.length); // bao gồm khoảng trắng đầu nếu có return ( <> {mention} {rest} ); }; const splitWords = (s: string) => s .trim() .split(/\s+/) .filter(Boolean); const renderTruncatedContent = (text: string | undefined, key: string) => { if (!text) return null; const maxWords = 35; const words = splitWords(text); const needsTruncate = words.length > maxWords; const expanded = isExpanded(key); const displayText = !needsTruncate || expanded ? text : `${words.slice(0, maxWords).join(" ")}...`; return ( <> {renderContentWithMention(displayText)} {needsTruncate && ( <> {" "} )} ); }; // Tính toán các parent comment cần hiển thị (bao gồm children) để hiển thị tối đa 5 comments tổng cộng const getDisplayedParents = () => { if (showAllComments) return { parents: parents, repliesMap: new Map() }; let totalCount = 0; const result: typeof parents = []; const limitedRepliesMap = new Map(); for (const parent of parents) { const allReplies = parent._id ? repliesByParent.get(parent._id) || [] : []; const remaining = 5 - totalCount; if (remaining <= 0) break; // Tính toán số lượng replies có thể hiển thị const repliesToShow = Math.min(allReplies.length, remaining - 1); // -1 cho chính parent comment if (repliesToShow >= 0) { result.push(parent); if (parent._id && repliesToShow > 0) { limitedRepliesMap.set(parent._id, allReplies.slice(0, repliesToShow)); } totalCount += 1 + repliesToShow; } else { // Không thể hiển thị cả parent, dừng lại break; } } return { parents: result, repliesMap: limitedRepliesMap }; }; const { parents: displayedParents, repliesMap: displayedRepliesMap } = getDisplayedParents(); // Tính số lượng comments hiển thị ban đầu (khi thu gọn, tối đa 5) const initialDisplayCount = displayedParents.reduce((sum, parent) => { // Khi thu gọn, sử dụng replies giới hạn từ displayedRepliesMap const replies = parent._id ? (displayedRepliesMap.get(parent._id) || []) : []; return sum + 1 + replies.length; }, 0); const hasMoreComments = (comments || []).length > initialDisplayCount; return (

{(comments || []).length} {(comments || []).length === 1 ? "Comment" : "Comments"}

{displayedParents.map((comment, index) => { // Khi showAllComments là false, chỉ sử dụng replies giới hạn từ displayedRepliesMap // Khi showAllComments là true, sử dụng tất cả replies từ repliesByParent const replies = comment._id ? (showAllComments ? (repliesByParent.get(comment._id) || []) : (displayedRepliesMap.get(comment._id) || [])) : []; const isReplyingHere = !!comment._id && replyTarget?.anchorId === comment._id; return (
{getInitials(comment.authorName)}
{formatLongDate(comment.createdAt)}

{comment.authorName}

{renderTruncatedContent( comment.content, makeCommentKey("p", comment._id, index) )}

{comment._id && ( )} {isReplyingHere && comment._id && (
setReplyTarget(null)} />
)} {/* Các reply */} {replies.length > 0 && (
{replies.map((reply: BlogComment, replyIndex: number) => (
{getInitials(reply.authorName)}
{formatLongDate(reply.createdAt)}

{reply.authorName}

{renderTruncatedContent( reply.content, makeCommentKey("r", reply._id, replyIndex) )}

{reply._id && comment._id && ( )}
{/* Form reply bên dưới child comment */} {reply._id && replyTarget && replyTarget.anchorId === reply._id && (
setReplyTarget(null)} />
)}
))}
)}
); })} {/* Nút hiển thị thêm comments */} {hasMoreComments && !showAllComments && (
)} {/* Nút thu gọn */} {hasMoreComments && showAllComments && (
)} {/* Form comment mới (top-level) */}
); }