"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 [replyTarget, setReplyTarget] = useState<{ // Root comment id to store as parentId (keeps 1-level threading on backend) parentId: string; // The author we are replying to (used for @mention UI) replyToName: string; // Which item in the UI is showing the reply form under it anchorId: string; } | null>(null); 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]); // Collect all author names to detect full @mentions (including multi-word names) const authorNames = useMemo(() => { return (comments || []) .map((c) => c.authorName) .filter((n): n is string => !!n) // Sort by length desc so we match the longest possible name first .sort((a, b) => b.length - a.length); }, [comments]); const renderContentWithMention = (text?: string) => { if (!text) return null; if (!text.startsWith("@")) return text; // Try to match an @mention that corresponds to a known author name. // This supports multi-word names like "@Bạn Cũng Thấy Thế Hà". const matchedName = authorNames.find((name) => { const candidate = `@${name}`; if (!text.startsWith(candidate)) return false; // Ensure the match ends at string end or is followed by a space const nextChar = text.charAt(candidate.length); return candidate.length === text.length || nextChar === " "; }); if (!matchedName) { // Fallback: just return the original text if we can't confidently match a name return text; } const mention = `@${matchedName}`; const rest = text.slice(mention.length); // includes leading space if present return ( <> {mention} {rest} ); }; return (

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

{parents.map((comment, index) => { const replies = comment._id ? repliesByParent.get(comment._id) || [] : []; const isReplyingHere = !!comment._id && replyTarget?.anchorId === comment._id; return (
{comment.authorName}
{formatLongDate(comment.createdAt)}

{comment.authorName}

{comment._id && ( )}

{renderContentWithMention(comment.content)}

{isReplyingHere && comment._id && (
setReplyTarget(null)} />
)} {/* Replies */} {replies.length > 0 && (
{replies.map((reply, replyIndex) => (
{reply.authorName}
{formatLongDate(reply.createdAt)}

{reply.authorName}

{reply._id && comment._id && ( )}

{renderContentWithMention(reply.content)}

{/* Reply form under child comment */} {reply._id && replyTarget?.anchorId === reply._id && (
setReplyTarget(null)} />
)}
))}
)}
); })} {/* New top-level comment */}
); }