'use client' import { Note, NOTE_COLORS, NoteColor } from '@/lib/types' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote } from 'lucide-react' import { useState, useEffect, useTransition, useOptimistic } from 'react' import { useSession } from 'next-auth/react' import { useRouter, useSearchParams } from 'next/navigation' import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, updateSize, getNoteAllUsers, leaveSharedNote, removeFusedBadge } from '@/app/actions/notes' import { cn } from '@/lib/utils' import { formatDistanceToNow, Locale } from 'date-fns' import * as dateFnsLocales from 'date-fns/locale' import { MarkdownContent } from './markdown-content' import { LabelBadge } from './label-badge' import { NoteImages } from './note-images' import { NoteChecklist } from './note-checklist' import { NoteActions } from './note-actions' import { CollaboratorDialog } from './collaborator-dialog' import { CollaboratorAvatars } from './collaborator-avatars' import { ConnectionsBadge } from './connections-badge' import { ConnectionsOverlay } from './connections-overlay' import { ComparisonModal } from './comparison-modal' import { useConnectionsCompare } from '@/hooks/use-connections-compare' import { useLabels } from '@/context/LabelContext' import { useLanguage } from '@/lib/i18n' import { useNotebooks } from '@/context/notebooks-context' // Mapping of supported languages to date-fns locales const localeMap: Record = { en: dateFnsLocales.enUS, fr: dateFnsLocales.fr, es: dateFnsLocales.es, de: dateFnsLocales.de, fa: dateFnsLocales.faIR, it: dateFnsLocales.it, pt: dateFnsLocales.pt, ru: dateFnsLocales.ru, zh: dateFnsLocales.zhCN, ja: dateFnsLocales.ja, ko: dateFnsLocales.ko, ar: dateFnsLocales.ar, hi: dateFnsLocales.hi, nl: dateFnsLocales.nl, pl: dateFnsLocales.pl, } function getDateLocale(language: string): Locale { return localeMap[language] || dateFnsLocales.enUS } interface NoteCardProps { note: Note onEdit?: (note: Note, readOnly?: boolean) => void isDragging?: boolean isDragOver?: boolean onDragStart?: (noteId: string) => void onDragEnd?: () => void } // Helper function to get initials from name function getInitials(name: string): string { if (!name) return '??' const trimmedName = name.trim() const parts = trimmedName.split(' ') if (parts.length === 1) { return trimmedName.substring(0, 2).toUpperCase() } return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() } // Helper function to get avatar color based on name hash function getAvatarColor(name: string): string { const colors = [ 'bg-blue-500', 'bg-purple-500', 'bg-green-500', 'bg-orange-500', 'bg-pink-500', 'bg-teal-500', 'bg-red-500', 'bg-indigo-500', ] const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) return colors[hash % colors.length] } export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, onDragEnd }: NoteCardProps) { const router = useRouter() const searchParams = useSearchParams() const { refreshLabels } = useLabels() const { data: session } = useSession() const { t, language } = useLanguage() const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks() const [isPending, startTransition] = useTransition() const [isDeleting, setIsDeleting] = useState(false) const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false) const [collaborators, setCollaborators] = useState([]) const [owner, setOwner] = useState(null) const [showConnectionsOverlay, setShowConnectionsOverlay] = useState(false) const [comparisonNotes, setComparisonNotes] = useState(null) const [showNotebookMenu, setShowNotebookMenu] = useState(false) // Move note to a notebook const handleMoveToNotebook = async (notebookId: string | null) => { await moveNoteToNotebookOptimistic(note.id, notebookId) setShowNotebookMenu(false) router.refresh() } const colorClasses = NOTE_COLORS[note.color as NoteColor] || NOTE_COLORS.default // Check if this note is currently open in the editor const isNoteOpenInEditor = searchParams.get('note') === note.id // Only fetch comparison notes when we have IDs to compare const { notes: comparisonNotesData, isLoading: isLoadingComparison } = useConnectionsCompare( comparisonNotes && comparisonNotes.length > 0 ? comparisonNotes : null ) // Optimistic UI state for instant feedback const [optimisticNote, addOptimisticNote] = useOptimistic( note, (state, newProps: Partial) => ({ ...state, ...newProps }) ) const currentUserId = session?.user?.id const canManageCollaborators = currentUserId && note.userId && currentUserId === note.userId const isSharedNote = currentUserId && note.userId && currentUserId !== note.userId const isOwner = currentUserId && note.userId && currentUserId === note.userId // Load collaborators when note changes useEffect(() => { const loadCollaborators = async () => { if (note.userId) { try { const users = await getNoteAllUsers(note.id) setCollaborators(users) // Owner is always first in the list if (users.length > 0) { setOwner(users[0]) } } catch (error) { console.error('Failed to load collaborators:', error) setCollaborators([]) } } } loadCollaborators() }, [note.id, note.userId]) const handleDelete = async () => { if (confirm(t('notes.confirmDelete'))) { setIsDeleting(true) try { await deleteNote(note.id) // Refresh global labels to reflect garbage collection await refreshLabels() } catch (error) { console.error('Failed to delete note:', error) setIsDeleting(false) } } } const handleTogglePin = async () => { startTransition(async () => { addOptimisticNote({ isPinned: !note.isPinned }) await togglePin(note.id, !note.isPinned) router.refresh() }) } const handleToggleArchive = async () => { startTransition(async () => { addOptimisticNote({ isArchived: !note.isArchived }) await toggleArchive(note.id, !note.isArchived) router.refresh() }) } const handleColorChange = async (color: string) => { startTransition(async () => { addOptimisticNote({ color }) await updateColor(note.id, color) router.refresh() }) } const handleSizeChange = async (size: 'small' | 'medium' | 'large') => { startTransition(async () => { addOptimisticNote({ size }) await updateSize(note.id, size) }) } const handleCheckItem = async (checkItemId: string) => { if (note.type === 'checklist' && note.checkItems) { const updatedItems = note.checkItems.map(item => item.id === checkItemId ? { ...item, checked: !item.checked } : item ) startTransition(async () => { addOptimisticNote({ checkItems: updatedItems }) await updateNote(note.id, { checkItems: updatedItems }) router.refresh() }) } } const handleLeaveShare = async () => { if (confirm(t('notes.confirmLeaveShare'))) { try { await leaveSharedNote(note.id) setIsDeleting(true) // Hide the note from view } catch (error) { console.error('Failed to leave share:', error) } } } const handleRemoveFusedBadge = async (e: React.MouseEvent) => { e.stopPropagation() // Prevent opening the note editor startTransition(async () => { addOptimisticNote({ autoGenerated: null }) await removeFusedBadge(note.id) router.refresh() }) } if (isDeleting) return null return ( { // Only trigger edit if not clicking on buttons const target = e.target as HTMLElement if (!target.closest('button') && !target.closest('[role="checkbox"]') && !target.closest('.drag-handle')) { // For shared notes, pass readOnly flag onEdit?.(note, !!isSharedNote) // Pass second parameter as readOnly flag (convert to boolean) } }} > {/* Move to Notebook Dropdown Menu */}
e.stopPropagation()} className="absolute top-2 right-2 z-20">
{t('notebookSuggestion.moveToNotebook')}
handleMoveToNotebook(null)}> {t('notebookSuggestion.generalNotes')} {notebooks.map((notebook: any) => ( handleMoveToNotebook(notebook.id)} > {notebook.icon || '📁'} {notebook.name} ))}
{/* Pin Button - Visible on hover or if pinned */} {/* Reminder Icon - Move slightly if pin button is there */} {note.reminder && new Date(note.reminder) > new Date() && ( )} {/* Memory Echo Badges - Fusion + Connections (BEFORE Title) */}
{/* Fusion Badge with remove button */} {note.autoGenerated && (
{t('memoryEcho.fused')}
)} {/* Connections Badge */} { // Only open overlay if note is NOT open in editor // (to avoid having 2 Dialogs with 2 close buttons) if (!isNoteOpenInEditor) { setShowConnectionsOverlay(true) } }} />
{/* Title */} {optimisticNote.title && (

{optimisticNote.title}

)} {/* Search Match Type Badge */} {optimisticNote.matchType && ( {t(`semanticSearch.${optimisticNote.matchType === 'exact' ? 'exactMatch' : 'related'}`)} )} {/* Shared badge */} {isSharedNote && owner && (
{t('notes.sharedBy')} {owner.name || owner.email}
)} {/* Images Component */} {/* Link Previews */} {optimisticNote.links && optimisticNote.links.length > 0 && (
{optimisticNote.links.map((link, idx) => ( e.stopPropagation()} > {link.imageUrl && ( )} {/* Content */} {optimisticNote.type === 'text' ? (
) : ( )} {/* Labels - ONLY show if note belongs to a notebook (labels are contextual per PRD) */} {optimisticNote.notebookId && optimisticNote.labels && optimisticNote.labels.length > 0 && (
{optimisticNote.labels.map((label) => ( ))}
)} {/* Footer with Date only */}
{/* Creation Date */}
{formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: getDateLocale(language) })}
{/* Owner Avatar - Aligned with action buttons at bottom */} {owner && (
{getInitials(owner.name || owner.email || '??')}
)} {/* Action Bar Component - Only for owner */} {isOwner && ( setShowCollaboratorDialog(true)} className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity" /> )} {/* Collaborator Dialog */} {currentUserId && note.userId && (
e.stopPropagation()}>
)} {/* Connections Overlay */}
e.stopPropagation()}> setShowConnectionsOverlay(false)} noteId={note.id} onOpenNote={(noteId) => { // Find the note and open it onEdit?.(note, false) }} onCompareNotes={(noteIds) => { setComparisonNotes(noteIds) }} />
{/* Comparison Modal */} {comparisonNotes && comparisonNotesData.length > 0 && (
e.stopPropagation()}> setComparisonNotes(null)} notes={comparisonNotesData} onOpenNote={(noteId) => { const foundNote = comparisonNotesData.find(n => n.id === noteId) if (foundNote) { onEdit?.(foundNote, false) } }} />
)} ) }