'use client' import React, { useState, useEffect, useCallback, useRef, useTransition, useMemo } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import dynamic from 'next/dynamic' import { Note } from '@/lib/types' import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote, deleteNote, togglePin, toggleArchive, updateNote, updateFullOrderWithoutRevalidation } from '@/app/actions/notes' import { NotesListViews, type NotesLayoutMode, type NotesClassicLayoutMode, isClassicLayoutMode } from '@/components/notes-list-views' import { NOTES_LAYOUT_STORAGE_KEY, parseNotesLayoutMode, setNotesLayoutPreference, } from '@/lib/notes-view-preference' import { useNotebookSchema } from '@/hooks/use-notebook-schema' import { bootstrapStructuredNotebook, ensureKanbanStatusField, type BootstrapStructuredTarget, } from '@/lib/structured-views/bootstrap-structured-notebook' import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast' import { Button } from '@/components/ui/button' import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, Tag as TagIcon, X, Menu, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload } from 'lucide-react' import { emitNoteChange } from '@/lib/note-change-sync' import { useReminderCheck } from '@/hooks/use-reminder-check' import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion' import { useNotebooks } from '@/context/notebooks-context' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' import { useEditorUI } from '@/context/editor-ui-context' import { NoteHistoryModal } from '@/components/note-history-modal' import { CreateNotebookDialog } from '@/components/create-notebook-dialog' import { StudyPlannerDialog } from '@/components/wizard/study-planner-dialog' import { NotebookOrganizerDialog } from '@/components/wizard/notebook-organizer-dialog' import { toast } from 'sonner' import { AnimatePresence, motion } from 'motion/react' type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual' const NoteEditor = dynamic( () => import('@/components/note-editor').then(m => ({ default: m.NoteEditor })), { ssr: false } ) const BatchOrganizationDialog = dynamic( () => import('@/components/batch-organization-dialog').then(m => ({ default: m.BatchOrganizationDialog })), { ssr: false } ) const AutoLabelSuggestionDialog = dynamic( () => import('@/components/auto-label-suggestion-dialog').then(m => ({ default: m.AutoLabelSuggestionDialog })), { ssr: false } ) const NotebookSummaryDialog = dynamic( () => import('@/components/notebook-summary-dialog').then(m => ({ default: m.NotebookSummaryDialog })), { ssr: false } ) const OrganizeNotebookDialog = dynamic( () => import('@/components/organize-notebook-dialog').then(m => ({ default: m.OrganizeNotebookDialog })), { ssr: false } ) const StructuredViewsIntro = dynamic( () => import('@/components/structured-views/structured-views-intro').then(m => ({ default: m.StructuredViewsIntro })), { ssr: false } ) const StructuredViewsHelpBanner = dynamic( () => import('@/components/structured-views/structured-views-help-banner').then(m => ({ default: m.StructuredViewsHelpBanner })), { ssr: false } ) const StructuredViewsContainer = dynamic( () => import('@/components/structured-views/structured-views-container').then(m => ({ default: m.StructuredViewsContainer })), { ssr: false } ) const AddPropertyDialog = dynamic( () => import('@/components/structured-views/add-property-dialog').then(m => ({ default: m.AddPropertyDialog })), { ssr: false } ) type InitialSettings = { showRecentNotes: boolean noteHistory: boolean noteHistoryMode: 'manual' | 'auto' aiAssistantEnabled: boolean } interface HomeClientProps { initialNotes: Note[] initialSettings: InitialSettings initialLayoutMode?: NotesLayoutMode } export function HomeClient({ initialNotes, initialSettings, initialLayoutMode = 'list', }: HomeClientProps) { const searchParams = useSearchParams() const router = useRouter() const { t } = useLanguage() const [notes, setNotes] = useState(initialNotes) const [pinnedNotes, setPinnedNotes] = useState( initialNotes.filter(n => n.isPinned) ) const [noteHistoryMode] = useState<'manual' | 'auto'>(initialSettings.noteHistoryMode) const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null) const [isLoading, setIsLoading] = useState(false) const [notebookSuggestion, setNotebookSuggestion] = useState<{ noteId: string; content: string } | null>(null) const [batchOrganizationOpen, setBatchOrganizationOpen] = useState(false) const [historyOpen, setHistoryOpen] = useState(false) const [historyNote, setHistoryNote] = useState(null) const [isCreating, startCreating] = useTransition() const [sortOrder, setSortOrder] = useState('newest') const [showSortMenu, setShowSortMenu] = useState(false) const [showInlineSearch, setShowInlineSearch] = useState(false) const [inlineSearchQuery, setInlineSearchQuery] = useState('') const [isSearching, setIsSearching] = useState(false) const inlineSearchRef = useRef(null) const searchDebounceRef = useRef | null>(null) const notesRef = useRef(notes) notesRef.current = notes const { labels, notebooks, refreshNotebooks } = useNotebooks() const labelsRef = useRef(labels) labelsRef.current = labels const initialNotesRef = useRef(initialNotes) initialNotesRef.current = initialNotes const { setControls } = useEditorUI() const { shouldSuggest: shouldSuggestLabels, notebookId: suggestNotebookId, dismiss: dismissLabelSuggestion } = useAutoLabelSuggestion() const [autoLabelOpen, setAutoLabelOpen] = useState(false) const [summaryDialogOpen, setSummaryDialogOpen] = useState(false) const [createSubNotebookOpen, setCreateSubNotebookOpen] = useState(false) const [organizeNotebookOpen, setOrganizeNotebookOpen] = useState(false) // kept for compat — old dialog unused const [selectedTagIds, setSelectedTagIds] = useState([]) const [isTagsExpanded, setIsTagsExpanded] = useState(false) const [tagSearchQuery, setTagSearchQuery] = useState('') const [layoutMode, setLayoutMode] = useState(initialLayoutMode) const [addPropertyOpen, setAddPropertyOpen] = useState(false) const [isEnablingStructured, setIsEnablingStructured] = useState(false) const [showStudyPlanner, setShowStudyPlanner] = useState(false) const [showOrganizer, setShowOrganizer] = useState(false) const handleExportCSV = useCallback(() => { if (!searchParams.get('notebook')) return window.open(`/api/notebooks/csv?notebookId=${searchParams.get('notebook')}`, '_blank') }, [searchParams]) const handleImportCSV = useCallback(() => { const input = document.createElement('input') input.type = 'file' input.accept = '.csv' input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0] if (!file) return const text = await file.text() const res = await fetch(`/api/notebooks/csv?notebookId=${searchParams.get('notebook')}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notebookId: searchParams.get('notebook'), csvData: text }), }) const data = await res.json() if (res.ok) { toast.success(`${data.created} notes importées !`) window.location.reload() } else { toast.error(data.error || 'Erreur') } } input.click() }, [searchParams]) const notebookFilter = searchParams.get('notebook') const schemaHook = useNotebookSchema(notebookFilter) const structuredModeActive = Boolean(notebookFilter && schemaHook.schema) const wantsStructuredView = Boolean( notebookFilter && (layoutMode === 'table' || layoutMode === 'kanban'), ) const structuredViewMode: BootstrapStructuredTarget = layoutMode === 'kanban' ? 'kanban' : 'table' useEffect(() => { if (layoutMode === 'gallery') { setLayoutMode('grid') } }, []) useEffect(() => { if (!notebookFilter && (layoutMode === 'kanban' || layoutMode === 'gallery')) { setLayoutMode('list') } }, [notebookFilter, layoutMode]) useEffect(() => { const storedLayout = parseNotesLayoutMode(localStorage.getItem(NOTES_LAYOUT_STORAGE_KEY)) if (storedLayout !== initialLayoutMode) { setLayoutMode(storedLayout) setNotesLayoutPreference(storedLayout) } }, [initialLayoutMode]) useEffect(() => { setNotesLayoutPreference(layoutMode) }, [layoutMode]) useEffect(() => { const onLayoutChange = (e: Event) => { const detail = (e as CustomEvent<{ layout?: NotesLayoutMode }>).detail?.layout if (detail === 'grid' || detail === 'list' || detail === 'table' || detail === 'kanban') { setLayoutMode(detail) } } window.addEventListener('memento-notes-layout-change', onLayoutChange) return () => window.removeEventListener('memento-notes-layout-change', onLayoutChange) }, []) // Sidebar carnet / inbox: fermer l'éditeur plein écran (comme la ref. architectural-grid) useEffect(() => { if (searchParams.get('forceList') === '1') { setEditingNote(null) const params = new URLSearchParams(searchParams.toString()) params.delete('forceList') const newUrl = params.toString() ? `/home?${params.toString()}` : '/home' router.replace(newUrl, { scroll: false }) } }, [searchParams, router]) const fetchNotesForCurrentView = useCallback( async (options?: { silent?: boolean }) => { const search = searchParams.get('search')?.trim() || null const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || [] const colorFilter = searchParams.get('color') const notebook = searchParams.get('notebook') const sharedOnly = searchParams.get('shared') === '1' const remindersOnly = searchParams.get('reminders') === '1' if (!options?.silent) { setIsLoading(true) } let allNotes = search ? await searchNotes(search, true, notebook || undefined) : await getAllNotes(false, notebook || undefined) if (sharedOnly) { allNotes = allNotes.filter((note: Note) => note._isShared) } else if (remindersOnly) { allNotes = allNotes.filter((note: Note) => note.reminder !== null) } else if (!notebook && !search) { allNotes = allNotes.filter((note: Note) => !note.notebookId && !note._isShared) } if (labelFilter.length > 0) { allNotes = allNotes.filter((note: Note) => labelFilter.every((label: string) => note.labels?.includes(label)) ) } if (colorFilter) { const labelNamesWithColor = labelsRef.current .filter((label) => label.color === colorFilter) .map((label) => label.name) allNotes = allNotes.filter((note: Note) => note.labels?.some((label: string) => labelNamesWithColor.includes(label)) ) } setNotes((prev) => { const localSizeMap = new Map(prev.map((n) => [n.id, n.size])) return allNotes.map((n) => ({ ...n, size: localSizeMap.get(n.id) ?? n.size })) }) setPinnedNotes(allNotes.filter((n: Note) => n.isPinned)) setIsLoading(false) }, [searchParams] ) const filterNotesForCurrentView = useCallback((source: Note[]) => { const notebook = searchParams.get('notebook') const sharedOnly = searchParams.get('shared') === '1' const remindersOnly = searchParams.get('reminders') === '1' if (notebook) { return source.filter((n) => n.notebookId === notebook && !n._isShared) } if (sharedOnly) { return source.filter((n) => n._isShared) } if (remindersOnly) { return source.filter((n) => n.reminder !== null) } return source.filter((n) => !n.notebookId && !n._isShared) }, [searchParams]) const handleNoteCreated = useCallback((note: Note) => { const search = searchParams.get('search')?.trim() || null if (search) { void fetchNotesForCurrentView({ silent: true }) emitNoteChange({ type: 'created', note }) return } setNotes((prevNotes) => { const notebookFilter = searchParams.get('notebook') const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || [] const colorFilter = searchParams.get('color') if (notebookFilter && note.notebookId !== notebookFilter) return prevNotes if (!notebookFilter && note.notebookId) return prevNotes if (labelFilter.length > 0) { const noteLabels = note.labels || [] if (!noteLabels.some((label: string) => labelFilter.includes(label))) return prevNotes } if (colorFilter) { const labelNamesWithColor = labels .filter((label: any) => label.color === colorFilter) .map((label: any) => label.name) const noteLabels = note.labels || [] if (!noteLabels.some((label: string) => labelNamesWithColor.includes(label))) return prevNotes } const isPinned = note.isPinned || false const pinnedNotes = prevNotes.filter(n => n.isPinned) const unpinnedNotes = prevNotes.filter(n => !n.isPinned) if (isPinned) { return [note, ...pinnedNotes, ...unpinnedNotes] } else { return [...pinnedNotes, note, ...unpinnedNotes] } }) emitNoteChange({ type: 'created', note }) if (!note.notebookId) { const wordCount = (note.content || '').trim().split(/\s+/).filter(w => w.length > 0).length if (wordCount >= 20) { setNotebookSuggestion({ noteId: note.id, content: note.content || '' }) } } }, [searchParams, labels, fetchNotesForCurrentView]) // Always fetch fresh from server — avoids stale state after a save regardless of // whether the notes list has re-fetched yet. const handleOpenNoteFresh = useCallback(async (noteId: string, readOnly = false) => { const note = await getNoteById(noteId) if (note) setEditingNote({ note, readOnly }) }, []) const handleAddNote = () => { startCreating(async () => { try { const newNote = await createNote({ content: '', type: 'richtext', title: undefined, notebookId: notebookFilter || undefined, skipRevalidation: true }) if (!newNote) return handleNoteCreated(newNote) setEditingNote({ note: newNote, readOnly: false }) } catch { toast.error(t('notes.createFailed')) } }) } const handleAddNoteWithProperties = (prefill: Record) => { startCreating(async () => { try { const newNote = await createNote({ content: '', type: 'richtext', title: undefined, notebookId: notebookFilter || undefined, skipRevalidation: true, }) if (!newNote) return if (Object.keys(prefill).length > 0) { await fetch(`/api/notes/${newNote.id}/properties`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ properties: prefill }), }) schemaHook.patchNoteValuesLocal(newNote.id, prefill) } handleNoteCreated(newNote) setEditingNote({ note: newNote, readOnly: false }) } catch { toast.error(t('notes.createFailed')) } }) } const structuredFieldLabels = useMemo( () => ({ statusName: t('structuredViews.wizard.fields.status.name'), statusOptions: t('structuredViews.wizard.fields.status.options') .split('\n') .map((line) => line.trim()) .filter(Boolean), }), [t], ) const structuredBootstrapActions = useMemo( () => ({ getSchema: () => schemaHook.schema, enableStructuredMode: schemaHook.enableStructuredMode, addProperty: schemaHook.addProperty, setKanbanGroupProperty: schemaHook.setKanbanGroupProperty, }), [schemaHook], ) const handleEnableStructured = useCallback( async (target: BootstrapStructuredTarget) => { if (!notebookFilter) return setIsEnablingStructured(true) try { await bootstrapStructuredNotebook(target, structuredFieldLabels, structuredBootstrapActions) await schemaHook.reload() setLayoutMode(target) toast.success(t('structuredViews.intro.enabledSuccess')) } catch { toast.error(t('structuredViews.enableFailed')) } finally { setIsEnablingStructured(false) } }, [notebookFilter, structuredFieldLabels, structuredBootstrapActions, schemaHook, t], ) const handleQuickAddKanbanStatus = useCallback(async () => { try { await ensureKanbanStatusField(structuredFieldLabels, structuredBootstrapActions) await schemaHook.reload() } catch { toast.error(t('structuredViews.enableFailed')) } }, [structuredFieldLabels, structuredBootstrapActions, schemaHook, t]) const selectLayoutMode = useCallback((mode: NotesLayoutMode) => { if (mode === 'gallery') return setLayoutMode(mode) }, []) const showStructuredIntro = wantsStructuredView && !structuredModeActive && !schemaHook.loading const showStructuredDataView = wantsStructuredView && structuredModeActive && schemaHook.schema const showStructuredLoading = wantsStructuredView && schemaHook.loading const classicLayoutMode: NotesClassicLayoutMode = isClassicLayoutMode(layoutMode) ? layoutMode : 'list' const handleOpenHistory = useCallback((note: Note) => { setHistoryNote(note) setHistoryOpen(true) }, []) const handleEnableHistory = useCallback(async (noteId: string) => { await enableNoteHistory(noteId) setNotes((prev) => prev.map((n) => (n.id === noteId ? { ...n, historyEnabled: true } : n))) setPinnedNotes((prev) => prev.map((n) => (n.id === noteId ? { ...n, historyEnabled: true } : n))) setEditingNote((prev) => (prev?.note.id === noteId ? { ...prev, note: { ...prev.note, historyEnabled: true } } : prev)) setHistoryNote((prev) => (prev?.id === noteId ? { ...prev, historyEnabled: true } : prev)) }, []) const handleHistoryRestored = useCallback((restored: Note) => { setNotes((prev) => prev.map((n) => (n.id === restored.id ? { ...n, ...restored } : n))) setPinnedNotes((prev) => prev.map((n) => (n.id === restored.id ? { ...n, ...restored } : n))) setEditingNote((prev) => (prev?.note.id === restored.id ? { ...prev, note: restored } : prev)) setHistoryNote((prev) => (prev?.id === restored.id ? { ...prev, ...restored } : prev)) }, []) const handleSizeChange = useCallback((noteId: string, size: 'small' | 'medium' | 'large') => { setNotes(prev => prev.map(n => n.id === noteId ? { ...n, size } : n)) setPinnedNotes(prev => prev.map(n => n.id === noteId ? { ...n, size } : n)) }, []) const patchNoteInList = useCallback((noteId: string, patch: Partial) => { setNotes((prev) => prev.map((n) => (n.id === noteId ? { ...n, ...patch } : n))) }, []) const removeNoteFromList = useCallback((noteId: string) => { setNotes((prev) => prev.filter((n) => n.id !== noteId)) setEditingNote((prev) => (prev?.note.id === noteId ? null : prev)) }, []) const noteVisibleInCurrentView = useCallback( (note: Pick) => { const notebook = searchParams.get('notebook') const sharedOnly = searchParams.get('shared') === '1' if (sharedOnly) return !!note._isShared if (notebook) return note.notebookId === notebook && !note._isShared return !note.notebookId && !note._isShared }, [searchParams] ) const handleTogglePin = useCallback( async (note: Note) => { const nextPinned = !note.isPinned patchNoteInList(note.id, { isPinned: nextPinned }) emitNoteChange({ type: 'updated', note: { ...note, isPinned: nextPinned } }) try { await togglePin(note.id, nextPinned, { skipRevalidation: true }) } catch { patchNoteInList(note.id, { isPinned: !nextPinned }) emitNoteChange({ type: 'updated', note: { ...note, isPinned: !nextPinned } }) toast.error(t('general.error')) } }, [patchNoteInList, t] ) const handleDeleteNoteFromList = useCallback( async (note: Note) => { removeNoteFromList(note.id) emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId }) try { await deleteNote(note.id, { skipRevalidation: true }) toast.success(t('notes.deleted') || 'Note supprimée') } catch { setNotes((prev) => [note, ...prev]) emitNoteChange({ type: 'created', note }) toast.error(t('general.error')) } }, [removeNoteFromList, t] ) const handleArchiveNoteFromList = useCallback( async (note: Note) => { const nextArchived = !note.isArchived if (nextArchived) removeNoteFromList(note.id) else patchNoteInList(note.id, { isArchived: nextArchived }) emitNoteChange({ type: 'updated', note: { ...note, isArchived: nextArchived } }) try { await toggleArchive(note.id, nextArchived, { skipRevalidation: true }) toast.success( nextArchived ? (t('notes.archived') || 'Archivée') : (t('notes.unarchived') || 'Désarchivée') ) } catch { if (nextArchived) setNotes((prev) => [note, ...prev]) else patchNoteInList(note.id, { isArchived: !nextArchived }) toast.error(t('general.error')) } }, [patchNoteInList, removeNoteFromList, t] ) const handleMoveNoteToNotebook = useCallback( async (note: Note, notebookId: string | null) => { const moved = { ...note, notebookId } if (noteVisibleInCurrentView(moved)) { patchNoteInList(note.id, { notebookId }) } else { removeNoteFromList(note.id) } emitNoteChange({ type: 'updated', note: moved }) try { await updateNote(note.id, { notebookId }, { skipRevalidation: true }) toast.success(t('notebookSuggestion.movedToNotebook') || 'Note déplacée') } catch { if (noteVisibleInCurrentView(note)) { patchNoteInList(note.id, { notebookId: note.notebookId ?? null }) } else { setNotes((prev) => [note, ...prev]) } toast.error(t('general.error')) } }, [noteVisibleInCurrentView, patchNoteInList, removeNoteFromList, t] ) const handleNoteContentPatch = useCallback((noteId: string, patch: Partial) => { setNotes((prev) => { const next = prev.map((n) => (n.id === noteId ? { ...n, ...patch } : n)) const updated = next.find((n) => n.id === noteId) if (updated) emitNoteChange({ type: 'updated', note: updated }) return next }) }, []) const handleNoteIllustrationGenerated = useCallback(async (noteId: string) => { const fresh = await getNoteById(noteId) if (!fresh) return patchNoteInList(noteId, fresh) emitNoteChange({ type: 'updated', note: fresh }) }, [patchNoteInList]) const handleNoteIllustrationDeleted = useCallback((noteId: string) => { patchNoteInList(noteId, { illustrationSvg: null }) setNotes((prev) => { const updated = prev.find((n) => n.id === noteId) if (updated) emitNoteChange({ type: 'updated', note: { ...updated, illustrationSvg: null } }) return prev }) }, [patchNoteInList]) const handleGridReorder = useCallback( async (orderedIds: string[]) => { setSortOrder('manual') setNotes((prev) => { const orderMap = new Map(orderedIds.map((id, index) => [id, index])) return prev.map((n) => (orderMap.has(n.id) ? { ...n, order: orderMap.get(n.id)! } : n)) }) try { await updateFullOrderWithoutRevalidation(orderedIds) } catch { toast.error(t('general.error')) } }, [t], ) // Garder openNote dans l'URL tant que l'éditeur est ouvert → le sidebar peut surligner la note (comme activeNoteId dans la ref.) useEffect(() => { const openNoteId = searchParams.get('openNote') if (!openNoteId) return let cancelled = false const run = async () => { // Always fetch fresh data from DB to avoid showing stale content after a save. // notesRef.current can be stale if the notes list hasn't re-fetched yet when the // user closes and re-opens the note quickly after saving. const note = await getNoteById(openNoteId) if (cancelled || !note) return setEditingNote({ note, readOnly: false }) } run() return () => { cancelled = true } }, [searchParams]) useEffect(() => { const handler = (e: Event) => { const { name } = (e as CustomEvent).detail const removeLabel = (note: 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 } } setNotes((prev) => prev.map(removeLabel)) setPinnedNotes((prev) => prev.map(removeLabel)) } window.addEventListener('label-deleted', handler) return () => window.removeEventListener('label-deleted', handler) }, []) useEffect(() => { const search = searchParams.get('search')?.trim() || null const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || [] const colorFilter = searchParams.get('color') const notebook = searchParams.get('notebook') const sharedOnly = searchParams.get('shared') === '1' const remindersOnly = searchParams.get('reminders') === '1' const hasActiveFilter = !!(search || labelFilter.length > 0 || colorFilter || sharedOnly || remindersOnly) if (hasActiveFilter || notebook) { void fetchNotesForCurrentView() return } const filtered = filterNotesForCurrentView(initialNotesRef.current) setNotes((prev) => { const localSizeMap = new Map(prev.map((n) => [n.id, n.size])) return filtered.map((n) => ({ ...n, size: localSizeMap.get(n.id) ?? n.size })) }) setPinnedNotes(filtered.filter((n) => n.isPinned)) }, [searchParams, fetchNotesForCurrentView, filterNotesForCurrentView]) const currentNotebook = notebooks.find((n: any) => n.id === searchParams.get('notebook')) const notebookPath = useMemo(() => { if (!currentNotebook) return [] const trail: any[] = [] let current: any = currentNotebook while (current) { trail.unshift(current) if (!current.parentId) break const parent = notebooks.find((nb: any) => nb.id === current.parentId) if (!parent) break current = parent } return trail }, [currentNotebook, notebooks]) const availableTags = useMemo(() => { const tagsMap = new Map() const carnetNotes = notes carnetNotes.forEach(note => { ;(note.labels || []).forEach(labelName => { if (!tagsMap.has(labelName)) { const labelObj = labels.find((l: any) => l.name === labelName) tagsMap.set(labelName, { id: labelObj?.id || labelName, name: labelName, type: labelObj?.type, }) } }) }) return Array.from(tagsMap.values()).sort((a, b) => { if (a.type === 'ai' && b.type !== 'ai') return -1 if (a.type !== 'ai' && b.type === 'ai') return 1 return a.name.localeCompare(b.name) }) }, [notes, labels]) const visibleTags = useMemo(() => { let filtered = availableTags if (tagSearchQuery) { filtered = availableTags.filter(t => t.name.toLowerCase().includes(tagSearchQuery.toLowerCase()) ) } else if (!isTagsExpanded) { filtered = availableTags.slice(0, 10) selectedTagIds.forEach(id => { if (!filtered.find(t => t.id === id)) { const tag = availableTags.find(t => t.id === id) if (tag) filtered.push(tag) } }) } return filtered }, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds]) const toggleTag = useCallback((tagId: string) => { setSelectedTagIds(prev => prev.includes(tagId) ? prev.filter(id => id !== tagId) : [...prev, tagId] ) }, []) useEffect(() => { setControls({ openNoteComposer: () => handleAddNote(), }) return () => setControls(null) }, [setControls]) // Apply sort order to notes const sortedNotes = useMemo(() => { let sorted = [...notes] if (sortOrder === 'newest') sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) if (sortOrder === 'oldest') sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) if (sortOrder === 'alpha') sorted.sort((a, b) => (a.title || '').localeCompare(b.title || '')) if (sortOrder === 'manual') sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) if (selectedTagIds.length > 0) { const selectedNames = selectedTagIds .map(id => availableTags.find(t => t.id === id)?.name) .filter(Boolean) as string[] sorted = sorted.filter(note => selectedNames.every(name => (note.labels || []).includes(name)) ) } return sorted }, [notes, sortOrder, selectedTagIds, availableTags]) const sortedPinnedNotes = useMemo(() => { return sortedNotes.filter(n => n.isPinned) }, [sortedNotes]) const sortLabels: Record = { newest: t('sidebar.sortNewest'), oldest: t('sidebar.sortOldest'), alpha: t('sidebar.sortAlpha'), manual: t('sidebar.sortManual'), } const handleEditorClose = useCallback(() => { setEditingNote(null) const params = new URLSearchParams(searchParams.toString()) params.delete('openNote') const qs = params.toString() router.replace(qs ? `/home?${qs}` : '/home', { scroll: false }) }, [router, searchParams]) const handleNoteSaved = useCallback((savedNote: Note) => { setNotes((prev) => prev.map((n) => (n.id === savedNote.id ? { ...n, ...savedNote } : n))) setEditingNote((prev) => (prev?.note.id === savedNote.id ? { ...prev, note: savedNote } : prev)) emitNoteChange({ type: 'updated', note: savedNote }) }, []) return (
{editingNote ? (
) : (
{/* Hamburger mobile — ouvre la sidebar */}
{currentNotebook && notebookPath.length > 0 && (
{notebookPath.map((nb: any, i: number) => ( {i > 0 && } {nb.name} ))}
)}

{currentNotebook ? currentNotebook.name : searchParams.get('shared') === '1' ? t('sidebar.sharedWithMe') : searchParams.get('reminders') === '1' ? t('sidebar.reminders') : t('notes.title')}

{currentNotebook && ( )} {/* Inline search — toggles an input within the toolbar */} {showInlineSearch ? (
{isSearching ? (
) : ( )} { const q = e.target.value setInlineSearchQuery(q) setIsSearching(true) if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current) searchDebounceRef.current = setTimeout(() => { const params = new URLSearchParams(searchParams.toString()) if (q.trim()) { params.set('search', q) } else { params.delete('search') } router.push(`/home?${params.toString()}`) setIsSearching(false) }, 300) }} onBlur={() => { if (!inlineSearchQuery) { setShowInlineSearch(false) } }} onKeyDown={e => { if (e.key === 'Escape') { if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current) setShowInlineSearch(false) setInlineSearchQuery('') setIsSearching(false) const params = new URLSearchParams(searchParams.toString()) params.delete('search') router.push(`/home?${params.toString()}`) } }} placeholder={t('search.placeholder')} className="w-36 sm:w-48 bg-transparent border-b border-foreground/20 focus:border-foreground outline-none text-[13px] text-foreground placeholder:text-muted-foreground/50 py-0.5 transition-colors" /> {inlineSearchQuery && ( )}
) : ( )} {!searchParams.get('notebook') && searchParams.get('shared') !== '1' && ( )}
{!notebookFilter && (