"use client";
import * as React from "react";
import CommentThread from "@/components/ui/comment-thread";
type Reaction = { emoji: string; count: number; reacted: boolean };
type Comment = {
id: string;
author: { id: string; name: string };
body: string;
createdAt: Date;
editedAt?: Date;
reactions?: Reaction[];
replies?: Comment[];
};
const CURRENT_USER_ID = "priya";
const INITIAL_COMMENTS: Comment[] = [
{
id: "c1",
author: { id: "amara", name: "Amara Okafor" },
body: "Should the empty state link to the import guide, or open the importer directly?",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 3),
reactions: [
{ emoji: "👍", count: 2, reacted: false },
{ emoji: "🎉", count: 0, reacted: false },
{ emoji: "👀", count: 0, reacted: false },
],
replies: [
{
id: "c2",
author: { id: "priya", name: "Priya Raman" },
body: "Open the importer. The guide is one click away from there, and most people already have a CSV.",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
reactions: [
{ emoji: "👍", count: 3, reacted: true },
{ emoji: "🎉", count: 0, reacted: false },
{ emoji: "👀", count: 0, reacted: false },
],
replies: [
{
id: "c3",
author: { id: "jonas", name: "Jonas Weber" },
body: "Agreed. Our funnel shows 70% drop-off on the guide page.",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
reactions: [
{ emoji: "👍", count: 0, reacted: false },
{ emoji: "🎉", count: 0, reacted: false },
{ emoji: "👀", count: 0, reacted: false },
],
},
],
},
],
},
];
function nextId() {
return Math.random().toString(36).slice(2, 9);
}
function mapTree(
list: Comment[],
id: string,
fn: (c: Comment) => Comment,
): Comment[] {
return list.map(function (comment) {
if (comment.id === id) return fn(comment);
if (comment.replies && comment.replies.length)
return Object.assign({}, comment, {
replies: mapTree(comment.replies, id, fn),
});
return comment;
});
}
function removeFromTree(list: Comment[], id: string): Comment[] {
return list
.filter(function (comment) {
return comment.id !== id;
})
.map(function (comment) {
return comment.replies && comment.replies.length
? Object.assign({}, comment, {
replies: removeFromTree(comment.replies, id),
})
: comment;
});
}
export default function CommentThreadDemo() {
const [comments, setComments] = React.useState<Comment[]>(INITIAL_COMMENTS);
const [resolved, setResolved] = React.useState(false);
function handleReply(parentId: string | null, body: string) {
const reply: Comment = {
id: nextId(),
author: { id: CURRENT_USER_ID, name: "Priya Raman" },
body: body,
createdAt: new Date(),
};
if (parentId === null) {
setComments(function (current) {
return current.concat([reply]);
});
return;
}
setComments(function (current) {
return mapTree(current, parentId, function (comment) {
return Object.assign({}, comment, {
replies: (comment.replies || []).concat([reply]),
});
});
});
}
function handleEdit(id: string, body: string) {
setComments(function (current) {
return mapTree(current, id, function (comment) {
return Object.assign({}, comment, { body: body, editedAt: new Date() });
});
});
}
function handleDelete(id: string) {
setComments(function (current) {
return removeFromTree(current, id);
});
}
function handleReact(id: string, emoji: string) {
setComments(function (current) {
return mapTree(current, id, function (comment) {
return Object.assign({}, comment, {
reactions: (comment.reactions || []).map(function (reaction) {
return reaction.emoji === emoji
? Object.assign({}, reaction, {
reacted: !reaction.reacted,
count: reaction.count + (reaction.reacted ? -1 : 1),
})
: reaction;
}),
});
});
});
}
return (
<div className="mx-auto w-full max-w-xl rounded-[var(--radius-card)] border border-line bg-surface p-6">
<CommentThread
comments={comments}
currentUserId={CURRENT_USER_ID}
onReply={handleReply}
onEdit={handleEdit}
onDelete={handleDelete}
onReact={handleReact}
resolved={resolved}
onResolvedChange={setResolved}
/>
</div>
);
}