From 8a1e0fb013c8f01d8a1019e862a3247cf2a8d73d Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 15 Jan 2026 20:45:16 +0200 Subject: [PATCH] feat(web): add comment item component --- .../app/preview/chat-comments/page.tsx | 87 ++++++++++ .../comment-item/comment-actions.tsx | 51 ++++++ .../comment-item/comment-item.tsx | 155 ++++++++++++++++++ .../chat-comments/comment-item/types.ts | 34 ++++ 4 files changed, 327 insertions(+) create mode 100644 surfsense_web/components/chat-comments/comment-item/comment-actions.tsx create mode 100644 surfsense_web/components/chat-comments/comment-item/comment-item.tsx create mode 100644 surfsense_web/components/chat-comments/comment-item/types.ts diff --git a/surfsense_web/app/preview/chat-comments/page.tsx b/surfsense_web/app/preview/chat-comments/page.tsx index 0c4194c61..9272f5f87 100644 --- a/surfsense_web/app/preview/chat-comments/page.tsx +++ b/surfsense_web/app/preview/chat-comments/page.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; import { CommentComposer } from "@/components/chat-comments/comment-composer/comment-composer"; +import { CommentItem } from "@/components/chat-comments/comment-item/comment-item"; +import type { CommentData } from "@/components/chat-comments/comment-item/types"; import { MemberMentionPicker } from "@/components/chat-comments/member-mention-picker/member-mention-picker"; import type { MemberOption } from "@/components/chat-comments/member-mention-picker/types"; @@ -38,6 +40,57 @@ const fakeMembersData: MemberOption[] = [ }, ]; +const fakeCommentsData: CommentData[] = [ + { + id: 1, + content: "This is a great response! @Alice Smith can you review?", + contentRendered: "This is a great response! @Alice Smith can you review?", + author: { + id: "550e8400-e29b-41d4-a716-446655440002", + displayName: "Bob Johnson", + email: "bob.johnson@example.com", + avatarUrl: null, + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + isEdited: false, + canEdit: true, + canDelete: true, + }, + { + id: 2, + content: "I checked this yesterday and it looks good.", + contentRendered: "I checked this yesterday and it looks good.", + author: { + id: "550e8400-e29b-41d4-a716-446655440001", + displayName: "Alice Smith", + email: "alice@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 86400000).toISOString(), + updatedAt: new Date(Date.now() - 3600000).toISOString(), + isEdited: true, + canEdit: false, + canDelete: true, + }, + { + id: 3, + content: "Thanks @Bob Johnson and @Alice Smith for the quick turnaround!", + contentRendered: "Thanks @Bob Johnson and @Alice Smith for the quick turnaround!", + author: { + id: "550e8400-e29b-41d4-a716-446655440004", + displayName: null, + email: "david.wilson@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 3600000 * 3).toISOString(), + updatedAt: new Date(Date.now() - 3600000 * 3).toISOString(), + isEdited: false, + canEdit: true, + canDelete: false, + }, +]; + export default function ChatCommentsPreviewPage() { const [highlightedIndex, setHighlightedIndex] = useState(0); const [selectedMember, setSelectedMember] = useState(null); @@ -80,6 +133,40 @@ export default function ChatCommentsPreviewPage() { )} + {/* Comment Item Section */} +
+

Comment Item

+

+ Hover over comments to see the action menu. Mentions are highlighted. +

+ +
+ {/* Comment with replies */} +
+ alert(`Edit comment ${id}`)} + onDelete={(id) => alert(`Delete comment ${id}`)} + onReply={(id) => alert(`Reply to comment ${id}`)} + /> + alert(`Edit reply ${id}`)} + onDelete={(id) => alert(`Delete reply ${id}`)} + /> +
+ + {/* Standalone comment */} + alert(`Edit comment ${id}`)} + onDelete={(id) => alert(`Delete comment ${id}`)} + onReply={(id) => alert(`Reply to comment ${id}`)} + /> +
+
+ {/* Member Mention Picker Section */}

Member Mention Picker (Standalone)

diff --git a/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx b/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx new file mode 100644 index 000000000..5a59c5d83 --- /dev/null +++ b/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { MoreHorizontal, Pencil, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import type { CommentActionsProps } from "./types"; + +export function CommentActions({ + canEdit, + canDelete, + onEdit, + onDelete, +}: CommentActionsProps) { + if (!canEdit && !canDelete) { + return null; + } + + return ( + + + + + + {canEdit && ( + + + Edit + + )} + {canDelete && ( + + + Delete + + )} + + + ); +} + diff --git a/surfsense_web/components/chat-comments/comment-item/comment-item.tsx b/surfsense_web/components/chat-comments/comment-item/comment-item.tsx new file mode 100644 index 000000000..1d5428268 --- /dev/null +++ b/surfsense_web/components/chat-comments/comment-item/comment-item.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { MessageSquare } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { CommentActions } from "./comment-actions"; +import type { CommentItemProps } from "./types"; + +function getInitials(name: string | null, email: string): string { + if (name) { + return name + .split(" ") + .map((part) => part[0]) + .join("") + .toUpperCase() + .slice(0, 2); + } + return email[0].toUpperCase(); +} + +function formatTimestamp(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + const timeStr = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + + if (diffMins < 1) { + return "Just now"; + } + + if (diffMins < 60) { + return `${diffMins}m ago`; + } + + if (diffHours < 24 && date.getDate() === now.getDate()) { + return `Today at ${timeStr}`; + } + + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if (date.getDate() === yesterday.getDate() && diffDays < 2) { + return `Yesterday at ${timeStr}`; + } + + if (diffDays < 7) { + const dayName = date.toLocaleDateString("en-US", { weekday: "long" }); + return `${dayName} at ${timeStr}`; + } + + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, + }) + ` at ${timeStr}`; +} + +function renderMentions(content: string): React.ReactNode { + const mentionPattern = /@(\w+(?:\s+\w+)*)/g; + const parts: React.ReactNode[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = mentionPattern.exec(content)) !== null) { + if (match.index > lastIndex) { + parts.push(content.slice(lastIndex, match.index)); + } + + parts.push( + + {match[0]} + + ); + + lastIndex = match.index + match[0].length; + } + + if (lastIndex < content.length) { + parts.push(content.slice(lastIndex)); + } + + return parts.length > 0 ? parts : content; +} + +export function CommentItem({ + comment, + onEdit, + onDelete, + onReply, + isReply = false, +}: CommentItemProps) { + const displayName = comment.author?.displayName || comment.author?.email.split("@")[0] || "Unknown"; + const email = comment.author?.email || ""; + + return ( +
+ + {comment.author?.avatarUrl && ( + + )} + + {getInitials(comment.author?.displayName ?? null, email || "U")} + + + +
+
+ {displayName} + + {formatTimestamp(comment.createdAt)} + + {comment.isEdited && ( + (edited) + )} +
+ onEdit?.(comment.id)} + onDelete={() => onDelete?.(comment.id)} + /> +
+
+ +
+ {renderMentions(comment.contentRendered)} +
+ + {!isReply && onReply && ( + + )} +
+
+ ); +} + diff --git a/surfsense_web/components/chat-comments/comment-item/types.ts b/surfsense_web/components/chat-comments/comment-item/types.ts new file mode 100644 index 000000000..8c5d841bc --- /dev/null +++ b/surfsense_web/components/chat-comments/comment-item/types.ts @@ -0,0 +1,34 @@ +export interface CommentAuthor { + id: string; + displayName: string | null; + email: string; + avatarUrl?: string | null; +} + +export interface CommentData { + id: number; + content: string; + contentRendered: string; + author: CommentAuthor | null; + createdAt: string; + updatedAt: string; + isEdited: boolean; + canEdit: boolean; + canDelete: boolean; +} + +export interface CommentItemProps { + comment: CommentData; + onEdit?: (commentId: number) => void; + onDelete?: (commentId: number) => void; + onReply?: (commentId: number) => void; + isReply?: boolean; +} + +export interface CommentActionsProps { + canEdit: boolean; + canDelete: boolean; + onEdit: () => void; + onDelete: () => void; +} +