// Dependencies: react ^18, lucide-react
import React, { useState, useRef } from 'react';
import { CornerDownRight, Send } from 'lucide-react';
function cn(...classes: (string | false | null | undefined)[]) {
return classes.filter(Boolean).join(' ');
}
type Reaction = { emoji: string; count: number; reacted: boolean };
type Comment = {
id: string;
author: { name: string; initials: string; color: string };
body: string;
timestamp: Date;
reactions?: Reaction[];
replies?: Comment[];
};
function formatRelative(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays === 1) return 'Yesterday';
return `${diffDays}d ago`;
}
function CommentItem({
comment,
onReply,
onReact,
depth = 0,
}: {
comment: Comment;
onReply?: (id: string) => void;
onReact?: (commentId: string, emoji: string) => void;
depth?: number;
}) {
return (
<div className={cn('flex gap-3', depth > 0 && 'ml-10 mt-2')}>
<div
className="w-7 h-7 rounded-full flex items-center justify-center text-white text-[11px] font-bold shrink-0 mt-0.5"
style={{ backgroundColor: comment.author.color }}
>
{comment.author.initials}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2 mb-1">
<span className="text-sm font-semibold text-white">{comment.author.name}</span>
<span className="text-xs text-zinc-600">{formatRelative(comment.timestamp)}</span>
</div>
<p className="text-sm text-zinc-300 leading-relaxed">{comment.body}</p>
{comment.reactions && comment.reactions.length > 0 && (
<div className="flex items-center gap-1 mt-2">
{comment.reactions.map((r) => (
<button
key={r.emoji}
onClick={() => onReact?.(comment.id, r.emoji)}
className={cn(
'flex items-center gap-1 px-2 py-0.5 rounded-full text-xs border transition-colors',
r.reacted
? 'bg-indigo-500/20 border-indigo-500/40 text-indigo-300'
: 'bg-zinc-800/60 border-zinc-700/60 text-zinc-400 hover:border-zinc-500'
)}
>
<span>{r.emoji}</span>
<span>{r.count}</span>
</button>
))}
</div>
)}
<button
onClick={() => onReply?.(comment.id)}
className="flex items-center gap-1 text-xs text-zinc-600 hover:text-zinc-400 transition-colors mt-2"
>
<CornerDownRight className="w-3 h-3" />
Reply
</button>
{comment.replies?.map((reply) => (
<CommentItem
key={reply.id}
comment={reply}
onReply={onReply}
onReact={onReact}
depth={depth + 1}
/>
))}
</div>
</div>
);
}
type Props = {
initialComments?: Comment[];
currentUser?: { name: string; initials: string; color: string };
};
const DEFAULT_USER = { name: 'You', initials: 'YO', color: '#6366f1' };
export function CommentThread({ initialComments = [], currentUser = DEFAULT_USER }: Props) {
const [comments, setComments] = useState<Comment[]>(initialComments);
const [draft, setDraft] = useState('');
const [replyingTo, setReplyingTo] = useState<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleSend = () => {
const body = draft.trim();
if (!body) return;
const newComment: Comment = {
id: `c-${Date.now()}`,
author: currentUser,
body,
timestamp: new Date(),
};
if (replyingTo) {
setComments((prev) =>
prev.map((c) =>
c.id === replyingTo
? { ...c, replies: [...(c.replies ?? []), newComment] }
: c
)
);
setReplyingTo(null);
} else {
setComments((prev) => [...prev, newComment]);
}
setDraft('');
};
const handleReact = (commentId: string, emoji: string) => {
setComments((prev) =>
prev.map((c) => {
if (c.id !== commentId) return c;
const reactions = (c.reactions ?? []).map((r) =>
r.emoji === emoji
? { ...r, count: r.reacted ? r.count - 1 : r.count + 1, reacted: !r.reacted }
: r
);
return { ...c, reactions };
})
);
};
const replyTarget = replyingTo ? comments.find((c) => c.id === replyingTo) : null;
return (
<div className="flex flex-col h-full bg-[#0a0a0a] border border-[#1a1a1a] rounded-xl overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-[#1a1a1a] shrink-0">
<span className="text-sm font-semibold text-white">Comments</span>
<span className="text-xs text-zinc-600">{comments.length}</span>
</div>
<div className="flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-4">
{comments.length === 0 && (
<div className="flex items-center justify-center h-full">
<p className="text-sm text-zinc-600">No comments yet</p>
</div>
)}
{comments.map((c) => (
<CommentItem
key={c.id}
comment={c}
onReply={(id) => {
setReplyingTo(id);
textareaRef.current?.focus();
}}
onReact={handleReact}
/>
))}
</div>
<div className="shrink-0 border-t border-[#1a1a1a] px-4 py-3 flex flex-col gap-2">
{replyTarget && (
<div className="flex items-center justify-between px-2 py-1 rounded-md bg-indigo-500/10 border border-indigo-500/20">
<span className="text-xs text-indigo-400">
Replying to <strong>{replyTarget.author.name}</strong>
</span>
<button
onClick={() => setReplyingTo(null)}
className="text-zinc-500 hover:text-zinc-300 text-xs"
>
✕
</button>
</div>
)}
<div className="flex items-end gap-2">
<div
className="w-7 h-7 rounded-full flex items-center justify-center text-white text-[11px] font-bold shrink-0"
style={{ backgroundColor: currentUser.color }}
>
{currentUser.initials}
</div>
<textarea
ref={textareaRef}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleSend();
}
}}
placeholder="Write a comment…"
rows={1}
className="flex-1 resize-none bg-[#111] border border-[#2a2a2a] rounded-lg px-3 py-2 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-indigo-500/50 transition-colors"
style={{ minHeight: '36px', maxHeight: '120px' }}
/>
<button
onClick={handleSend}
disabled={!draft.trim()}
className="w-8 h-8 rounded-lg bg-indigo-500 hover:bg-indigo-400 disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center transition-colors shrink-0"
>
<Send className="w-3.5 h-3.5 text-white" />
</button>
</div>
<p className="text-[11px] text-zinc-700 text-right">⌘+Enter to send</p>
</div>
</div>
);
}