'use client' import { useState, useEffect, useCallback } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import dynamic from 'next/dynamic' import { Note } from '@/lib/types' import { getAISettings } from '@/app/actions/ai-settings' import { getAllNotes, searchNotes } from '@/app/actions/notes' import { NoteInput } from '@/components/note-input' import { NotesMainSection, type NotesViewMode } from '@/components/notes-main-section' import { NotesViewToggle } from '@/components/notes-view-toggle' import { MemoryEchoNotification } from '@/components/memory-echo-notification' import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast' import { FavoritesSection } from '@/components/favorites-section' import { Button } from '@/components/ui/button' import { Wand2, ChevronRight, Plus, FileText } from 'lucide-react' import { useLabels } from '@/context/LabelContext' import { useNoteRefresh } from '@/context/NoteRefreshContext' import { useReminderCheck } from '@/hooks/use-reminder-check' import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion' import { useNotebooks } from '@/context/notebooks-context' import { getNotebookIcon } from '@/lib/notebook-icon' import { cn } from '@/lib/utils' import { LabelFilter } from '@/components/label-filter' import { useLanguage } from '@/lib/i18n' import { useHomeView } from '@/context/home-view-context' // Lazy-load heavy dialogs — uniquement chargés à la demande 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 } ) type InitialSettings = { showRecentNotes: boolean notesViewMode: 'masonry' | 'tabs' } interface HomeClientProps { initialNotes: Note[] initialSettings: InitialSettings } export function HomeClient({ initialNotes, initialSettings }: 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 [notesViewMode, setNotesViewMode] = useState(initialSettings.notesViewMode) const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null) const [isLoading, setIsLoading] = useState(false) // false by default — data is pre-loaded const [notebookSuggestion, setNotebookSuggestion] = useState<{ noteId: string; content: string } | null>(null) const [batchOrganizationOpen, setBatchOrganizationOpen] = useState(false) const { refreshKey } = useNoteRefresh() const { labels } = useLabels() const { setControls } = useHomeView() const { shouldSuggest: shouldSuggestLabels, notebookId: suggestNotebookId, dismiss: dismissLabelSuggestion } = useAutoLabelSuggestion() const [autoLabelOpen, setAutoLabelOpen] = useState(false) useEffect(() => { if (shouldSuggestLabels && suggestNotebookId) { setAutoLabelOpen(true) } }, [shouldSuggestLabels, suggestNotebookId]) const notebookFilter = searchParams.get('notebook') const isInbox = !notebookFilter const handleNoteCreated = useCallback((note: Note) => { setNotes((prevNotes) => { const notebookFilter = searchParams.get('notebook') const labelFilter = searchParams.get('labels')?.split(',').filter(Boolean) || [] const colorFilter = searchParams.get('color') const search = searchParams.get('search')?.trim() || null 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 } if (search) { router.refresh() 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] } }) 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, router]) const handleOpenNote = (noteId: string) => { const note = notes.find(n => n.id === noteId) if (note) setEditingNote({ note, readOnly: false }) } 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)) }, []) useReminderCheck(notes) // Rechargement uniquement pour les filtres actifs (search, labels, notebook) // Les notes initiales suffisent sans filtre 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 semanticMode = searchParams.get('semantic') === 'true' // Pour le refreshKey (mutations), toujours recharger // Pour les filtres, charger depuis le serveur const hasActiveFilter = search || labelFilter.length > 0 || colorFilter const load = async () => { setIsLoading(true) let allNotes = search ? await searchNotes(search, semanticMode, notebook || undefined) : await getAllNotes() // Filtre notebook côté client // Shared notes appear ONLY in inbox (general notes), not in notebooks if (notebook) { allNotes = allNotes.filter((note: any) => note.notebookId === notebook && !note._isShared) } else { allNotes = allNotes.filter((note: any) => !note.notebookId || note._isShared) } // Filtre labels if (labelFilter.length > 0) { allNotes = allNotes.filter((note: any) => note.labels?.some((label: string) => labelFilter.includes(label)) ) } // Filtre couleur if (colorFilter) { const labelNamesWithColor = labels .filter((label: any) => label.color === colorFilter) .map((label: any) => label.name) allNotes = allNotes.filter((note: any) => note.labels?.some((label: string) => labelNamesWithColor.includes(label)) ) } // Merger avec les tailles locales pour ne pas écraser les modifications 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: any) => n.isPinned)) setIsLoading(false) } // Éviter le rechargement initial si les notes sont déjà chargées sans filtres if (refreshKey > 0 || hasActiveFilter) { const cancelled = { value: false } load().then(() => { if (cancelled.value) return }) return () => { cancelled.value = true } } else { // Données initiales : filtrage inbox/notebook côté client seulement let filtered = initialNotes if (notebook) { filtered = initialNotes.filter((n: any) => n.notebookId === notebook && !n._isShared) } else { filtered = initialNotes.filter((n: any) => !n.notebookId || n._isShared) } // Merger avec les tailles déjà modifiées localement 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)) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams, refreshKey]) const { notebooks } = useNotebooks() const currentNotebook = notebooks.find((n: any) => n.id === searchParams.get('notebook')) useEffect(() => { setControls({ isTabsMode: notesViewMode === 'tabs', openNoteComposer: () => {}, }) return () => setControls(null) }, [notesViewMode, setControls]) const handleNoteCreatedWrapper = (note: any) => { handleNoteCreated(note) } const Breadcrumbs = ({ notebookName }: { notebookName: string }) => (
{t('nav.notebooks')} {notebookName}
) const isTabs = notesViewMode === 'tabs' return (
{/* Notebook Specific Header */} {currentNotebook ? (
{(() => { const Icon = getNotebookIcon(currentNotebook.icon || 'folder') return ( ) })()}

{currentNotebook.name}

{ const params = new URLSearchParams(searchParams.toString()) if (newLabels.length > 0) params.set('labels', newLabels.join(',')) else params.delete('labels') router.push(`/?${params.toString()}`) }} className="border-gray-200" />
) : (
{!isTabs &&
}

{t('notes.title')}

{ const params = new URLSearchParams(searchParams.toString()) if (newLabels.length > 0) params.set('labels', newLabels.join(',')) else params.delete('labels') router.push(`/?${params.toString()}`) }} className="border-gray-200" /> {isInbox && !isLoading && notes.length >= 2 && ( )}
)} {!isTabs && (
)} {isLoading ? (
{t('general.loading')}
) : ( <> setEditingNote({ note, readOnly })} onSizeChange={handleSizeChange} /> {notes.filter((note) => !note.isPinned).length > 0 && (
!note.isPinned)} onEdit={(note, readOnly) => setEditingNote({ note, readOnly })} onSizeChange={handleSizeChange} currentNotebookId={searchParams.get('notebook')} />
)} {notes.filter(note => !note.isPinned).length === 0 && pinnedNotes.length === 0 && (
{t('notes.emptyState')}
)} )} {notebookSuggestion && ( setNotebookSuggestion(null)} /> )} {batchOrganizationOpen && ( router.refresh()} /> )} {autoLabelOpen && ( { setAutoLabelOpen(open) if (!open) dismissLabelSuggestion() }} notebookId={suggestNotebookId} onLabelsCreated={() => router.refresh()} /> )} {editingNote && ( setEditingNote(null)} /> )}
) }