'use client' import { useCallback, useEffect, useMemo, useState, useTransition } from 'react' import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' 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, updateNote, toggleArchive, } from '@/app/actions/notes' import { useNotebooks } from '@/context/notebooks-context' import { GripVertical, ListChecks, Pin, PinOff, FileText, Plus, Loader2, Trash2, ListFilter, FolderInput, Archive, Share2, Check, Hash, History, PanelRightClose, PanelRightOpen, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' import { toast } from 'sonner' import { format, type Locale } 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 noteHistoryMode?: 'manual' | 'auto' onOpenHistory?: (note: Note) => void onEnableHistory?: (noteId: string) => Promise onNoteCreated?: (note: Note) => void } type SortOrder = 'date-desc' | 'date-asc' | 'title-asc' | 'title-desc' // 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 } function formatNoteDate(date: Date | string, locale: Locale): string { const d = typeof date === 'string' ? new Date(date) : date return format(d, 'd MMM yyyy', { locale }) } function countWords(content: string | null | undefined): number { if (!content) return 0 return content.trim().split(/\s+/).filter(Boolean).length } // ─── 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, 200) : (note.content || '').substring(0, 200) const dateLocale = getDateLocale(language) const dateStr = formatNoteDate(note.updatedAt, dateLocale) return (
{/* Left accent bar — solid when selected, transparent otherwise */}
{/* Main card content */}
{/* Row 1: type icon + date */}
{note.type === 'checklist' ? ( ) : ( )} {note.isPinned && ( )}
{dateStr}
{/* Row 2: title */}

{title}

{/* Row 3: snippet */} {snippet && (

{snippet}

)} {/* Row 4: label chips */} {Array.isArray(note.labels) && note.labels.length > 0 && (
{note.labels.slice(0, 3).map((label) => ( {label} ))} {note.labels.length > 3 && ( +{note.labels.length - 3} )}
)}
{/* Actions column: drag + delete on hover */}
) } // ─── Note Meta Sidebar ──────────────────────────────────────────────────────── function SidebarSection({ title }: { title: string }) { return (

{title}

) } function SidebarActionBtn({ icon, label, onClick, disabled = false, }: { icon: React.ReactNode label: string onClick: () => void disabled?: boolean }) { return ( ) } function NoteMetaSidebar({ note, onPinToggle, onArchive, onOpenHistory, onEnableHistory, }: { note: Note onPinToggle: (note: Note) => void onArchive: (note: Note) => void onOpenHistory?: (note: Note) => void onEnableHistory?: (noteId: string) => Promise }) { const { t } = useLanguage() const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks() const [moveOpen, setMoveOpen] = useState(false) const [isMoving, setIsMoving] = useState(false) // t() returns the key itself when not found — use this wrapper for safe fallbacks const ts = (key: string, fallback: string) => { const v = t(key as Parameters[0]) return v === key ? fallback : v } const wordCount = countWords(note.content) const noteTypeLabel = note.type === 'checklist' ? ts('notes.typeChecklist', 'Checklist') : note.isMarkdown ? ts('notes.typeMarkdown', 'Markdown') : ts('notes.typeText', 'Text') const handleMoveToNotebook = async (notebookId: string | null) => { setIsMoving(true) try { await moveNoteToNotebookOptimistic(note.id, notebookId) setMoveOpen(false) toast.success(ts('notebookSuggestion.movedToNotebook', 'Note déplacée')) } catch { toast.error(ts('notes.moveFailed', 'Déplacement échoué')) } finally { setIsMoving(false) } } const handleHistory = async () => { if (!note.historyEnabled) { if (!onEnableHistory) { toast.info(ts('notes.historyDisabledDesc', "L'historique est désactivé pour cette note.")) return } try { await onEnableHistory(note.id) toast.success(ts('notes.historyEnabled', 'Historique activé')) onOpenHistory?.(note) } catch { toast.error(ts('general.error', 'Erreur')) } return } onOpenHistory?.(note) } return ( ) } // ─── Main Component ─────────────────────────────────────────────────────────── export function NotesTabsView({ notes, onEdit, currentNotebookId, noteHistoryMode = 'manual', onOpenHistory, onEnableHistory, onNoteCreated, }: NotesTabsViewProps) { const { t, language } = useLanguage() const { triggerRefresh } = useNoteRefreshOptional() const [items, setItems] = useState(notes) const [selectedId, setSelectedId] = useState(null) const [isCreating, startCreating] = useTransition() const [noteToDelete, setNoteToDelete] = useState(null) const [sortOrder, setSortOrder] = useState('date-desc') const [sidebarOpen, setSidebarOpen] = useState(true) useEffect(() => { setItems((prev) => { const prevIds = prev.map((n) => n.id).join(',') const incomingIds = notes.map((n) => n.id).join(',') const merge = (fresh: Note, local: Note) => { const labelsChanged = JSON.stringify(fresh.labels?.sort()) !== JSON.stringify(local.labels?.sort()) return { ...fresh, title: local.title || fresh.title, content: local.content, checkItems: local.checkItems, labels: labelsChanged ? fresh.labels : local.labels } } if (prevIds === incomingIds) { return prev.map((p) => { const fresh = notes.find((n) => n.id === p.id) if (!fresh) return p return merge(fresh, p) }) } return notes.map((fresh) => { const local = prev.find((p) => p.id === fresh.id) if (!local) return fresh return merge(fresh, local) }) }) }, [notes]) useEffect(() => { if (items.length === 0) { setSelectedId(null) return } setSelectedId((prev) => prev && items.some((n) => n.id === prev) ? prev : items[0].id ) }, [items]) useEffect(() => { const handler = (e: Event) => { const { name } = (e as CustomEvent).detail if (!name) return setItems((prev) => prev.map((note) => { const currentLabels = note.labels || [] const updated = currentLabels.filter((l) => l.toLowerCase() !== name.toLowerCase()) if (updated.length === currentLabels.length) return note return { ...note, labels: updated.length > 0 ? updated : null } }) ) } window.addEventListener('label-deleted', handler) return () => window.removeEventListener('label-deleted', handler) }, []) // Sorted display items — pinned notes always float to the top const sortedItems = useMemo(() => { const pinned = items.filter(n => n.isPinned) const unpinned = items.filter(n => !n.isPinned) const sortFn = (a: Note, b: Note) => { if (sortOrder === 'date-desc') return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() if (sortOrder === 'date-asc') return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() if (sortOrder === 'title-asc') return (a.title || '').localeCompare(b.title || '') if (sortOrder === 'title-desc') return (b.title || '').localeCompare(a.title || '') return 0 } return [...pinned.sort(sortFn), ...unpinned.sort(sortFn)] }, [items, sortOrder]) 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' const handleCreateNote = () => { startCreating(async () => { try { const newNote = await createNote({ content: '', title: undefined, notebookId: currentNotebookId || undefined, skipRevalidation: true }) if (!newNote) return setItems((prev) => { const pinned = prev.filter(n => n.isPinned) const unpinned = prev.filter(n => !n.isPinned) return [...pinned, newNote, ...unpinned] }) setSelectedId(newNote.id) onNoteCreated?.(newNote) triggerRefresh() } catch { toast.error(t('notes.createFailed') || 'Impossible de créer la note') } }) } const handlePinToggle = async (note: Note) => { const next = !note.isPinned setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: next } : n)) try { await updateNote(note.id, { isPinned: next }, { skipRevalidation: true }) triggerRefresh() toast.success(next ? (t('notes.pinned') || 'Épinglée') : (t('notes.unpinned') || 'Désépinglée')) } catch { setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: note.isPinned } : n)) toast.error(t('notes.updateFailed') || 'Mise à jour échouée') } } const handleArchive = async (note: Note) => { try { await toggleArchive(note.id, true) setItems((prev) => prev.filter((n) => n.id !== note.id)) setSelectedId((prev) => (prev === note.id ? null : prev)) triggerRefresh() toast.success(t('notes.archived') || 'Note archivée') } catch { toast.error(t('notes.archiveFailed') || 'Archivage échoué') } } return (
{/* ── Left panel: note list ── */}
{/* Header */}
{t('notes.title')} {items.length}
{/* Sort / filter button */} {t('notes.sortBy') || 'Trier par'} setSortOrder(v as SortOrder)}> {t('notes.sortDateDesc') || 'Date (récent)'} {t('notes.sortDateAsc') || 'Date (ancien)'} {t('notes.sortTitleAsc') || 'Titre A → Z'} {t('notes.sortTitleDesc') || 'Titre Z → A'} {/* New note button */}
{/* Scrollable note list */}
{items.length === 0 ? (

{t('notes.emptyStateTabs') || 'Aucune note'}

{t('notes.createFirst') || 'Créez votre première note'}

) : ( n.id)} strategy={verticalListSortingStrategy} >
{sortedItems.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 ── */} {selected ? (
{/* Editor */}
{ 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)) triggerRefresh() }} /> {/* Toggle sidebar button — top-right of editor, always visible */}
{/* Meta sidebar — collapsible */} {sidebarOpen && ( )}
) : (

{items.length === 0 ? t('notes.emptyNotebook') : t('notes.noNoteSelected')}

{items.length === 0 ? t('notes.emptyNotebookDesc') : t('notes.selectOrCreateNote')}

)} {/* 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'))}" )}
) }