'use client' import { useCallback, useEffect, useState, useTransition } from 'react' import { DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core' import { SortableContext, arrayMove, verticalListSortingStrategy, sortableKeyboardCoordinates, useSortable, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Note, NOTE_COLORS, NoteColor } from '@/lib/types' import { cn } from '@/lib/utils' import { NoteInlineEditor } from '@/components/note-inline-editor' import { useLanguage } from '@/lib/i18n' import { getNoteDisplayTitle } from '@/lib/note-preview' import { updateFullOrderWithoutRevalidation, createNote, deleteNote } from '@/app/actions/notes' import { GripVertical, Hash, ListChecks, Pin, FileText, Clock, Plus, Loader2, Trash2, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { toast } from 'sonner' import { formatDistanceToNow } from 'date-fns' import { fr } from 'date-fns/locale/fr' import { enUS } from 'date-fns/locale/en-US' interface NotesTabsViewProps { notes: Note[] onEdit?: (note: Note, readOnly?: boolean) => void currentNotebookId?: string | null } // Color accent strip for each note const COLOR_ACCENT: Record = { default: 'bg-primary', red: 'bg-red-400', orange: 'bg-orange-400', yellow: 'bg-amber-400', green: 'bg-emerald-400', teal: 'bg-teal-400', blue: 'bg-sky-400', purple: 'bg-violet-400', pink: 'bg-fuchsia-400', gray: 'bg-gray-400', } // Background tint gradient for selected note panel const COLOR_PANEL_BG: Record = { default: 'from-background to-background', red: 'from-red-50/60 dark:from-red-950/20 to-background', orange: 'from-orange-50/60 dark:from-orange-950/20 to-background', yellow: 'from-amber-50/60 dark:from-amber-950/20 to-background', green: 'from-emerald-50/60 dark:from-emerald-950/20 to-background', teal: 'from-teal-50/60 dark:from-teal-950/20 to-background', blue: 'from-sky-50/60 dark:from-sky-950/20 to-background', purple: 'from-violet-50/60 dark:from-violet-950/20 to-background', pink: 'from-fuchsia-50/60 dark:from-fuchsia-950/20 to-background', gray: 'from-gray-50/60 dark:from-gray-900/20 to-background', } const COLOR_ICON: Record = { default: 'text-primary', red: 'text-red-500', orange: 'text-orange-500', yellow: 'text-amber-500', green: 'text-emerald-500', teal: 'text-teal-500', blue: 'text-sky-500', purple: 'text-violet-500', pink: 'text-fuchsia-500', gray: 'text-gray-500', } function getColorKey(note: Note): NoteColor { return (typeof note.color === 'string' && note.color in NOTE_COLORS ? note.color : 'default') as NoteColor } function getDateLocale(language: string) { if (language === 'fr') return fr; if (language === 'fa') return require('date-fns/locale').faIR; return enUS; } // ─── Sortable List Item ─────────────────────────────────────────────────────── function SortableNoteListItem({ note, selected, onSelect, onDelete, reorderLabel, deleteLabel, language, untitledLabel, }: { note: Note selected: boolean onSelect: () => void onDelete: () => void reorderLabel: string deleteLabel: string language: string untitledLabel: string }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: note.id, }) const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 50 : undefined, } const ck = getColorKey(note) const title = getNoteDisplayTitle(note, untitledLabel) const snippet = note.type === 'checklist' ? (note.checkItems?.map((i) => i.text).join(' · ') || '').substring(0, 150) : (note.content || '').substring(0, 150) const dateLocale = getDateLocale(language) const timeAgo = formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale, }) return (
{/* Color accent bar */}
{/* Drag handle */} {/* Note type icon */}
{note.type === 'checklist' ? ( ) : ( )}
{/* Text content */}

{title}

{note.isPinned && ( )}
{snippet && (

{snippet}

)}
{timeAgo} {Array.isArray(note.labels) && note.labels.length > 0 && ( <> ·
{note.labels.slice(0, 2).join(', ')} {note.labels.length > 2 && ` +${note.labels.length - 2}`}
)}
{/* Delete button - visible on hover */}
) } // ─── Main Component ─────────────────────────────────────────────────────────── export function NotesTabsView({ notes, onEdit, currentNotebookId }: NotesTabsViewProps) { const { t, language } = useLanguage() const [items, setItems] = useState(notes) const [selectedId, setSelectedId] = useState(null) const [isCreating, startCreating] = useTransition() const [noteToDelete, setNoteToDelete] = useState(null) useEffect(() => { // Only reset when notes are added or removed, NOT on content/field changes // Field changes arrive through onChange -> setItems already setItems((prev) => { const prevIds = prev.map((n) => n.id).join(',') const incomingIds = notes.map((n) => n.id).join(',') if (prevIds === incomingIds) { // Same set of notes: merge only structural fields (pin, color, archive) return prev.map((p) => { const fresh = notes.find((n) => n.id === p.id) if (!fresh) return p // Use fresh labels from server if they've changed (e.g., global label deletion) const labelsChanged = JSON.stringify(fresh.labels?.sort()) !== JSON.stringify(p.labels?.sort()) return { ...fresh, title: p.title, content: p.content, // Always use server labels if different (for global label changes) labels: labelsChanged ? fresh.labels : p.labels } }) } // Different set (add/remove): full sync return notes }) }, [notes]) useEffect(() => { if (items.length === 0) { setSelectedId(null) return } setSelectedId((prev) => prev && items.some((n) => n.id === prev) ? prev : items[0].id ) }, [items]) // Scroll to top of sidebar on note change handled by NoteInlineEditor internally const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) const handleDragEnd = useCallback( async (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return const oldIndex = items.findIndex((n) => n.id === active.id) const newIndex = items.findIndex((n) => n.id === over.id) if (oldIndex < 0 || newIndex < 0) return const reordered = arrayMove(items, oldIndex, newIndex) setItems(reordered) try { await updateFullOrderWithoutRevalidation(reordered.map((n) => n.id)) } catch { setItems(notes) toast.error(t('notes.moveFailed')) } }, [items, notes, t] ) const selected = items.find((n) => n.id === selectedId) ?? null const colorKey = selected ? getColorKey(selected) : 'default' /** Create a new blank note, add it to the sidebar and select it immediately */ const handleCreateNote = () => { startCreating(async () => { try { const newNote = await createNote({ content: '', title: undefined, notebookId: currentNotebookId || undefined, skipRevalidation: true }) if (!newNote) return setItems((prev) => [newNote, ...prev]) setSelectedId(newNote.id) } catch { toast.error(t('notes.createFailed') || 'Impossible de créer la note') } }) } if (items.length === 0) { return (

{t('notes.emptyStateTabs')}

) } return (
{/* ── Left sidebar: note list ── */}
{/* Sidebar header with note count + new note button */}
{t('notes.title')} {items.length}
{/* Scrollable note list */}
n.id)} strategy={verticalListSortingStrategy} >
{items.map((note) => ( setSelectedId(note.id)} onDelete={() => setNoteToDelete(note)} reorderLabel={t('notes.reorderTabs')} deleteLabel={t('notes.delete')} language={language} untitledLabel={t('notes.untitled')} /> ))}
{/* ── Right content panel — always in edit mode ── */} {selected ? (
{ setItems((prev) => prev.map((n) => (n.id === noteId ? { ...n, ...fields } : n)) ) }} onDelete={(noteId) => { setItems((prev) => prev.filter((n) => n.id !== noteId)) setSelectedId((prev) => (prev === noteId ? null : prev)) }} onArchive={(noteId) => { setItems((prev) => prev.filter((n) => n.id !== noteId)) setSelectedId((prev) => (prev === noteId ? null : prev)) }} />
) : (

{t('notes.selectNote') || 'Sélectionnez une note'}

)} {/* Delete Confirmation Dialog */} setNoteToDelete(null)}> {t('notes.confirmDeleteTitle') || t('notes.delete')} {t('notes.confirmDelete') || 'Are you sure you want to delete this note?'} {noteToDelete && ( "{getNoteDisplayTitle(noteToDelete, t('notes.untitled'))}" )}
) }