'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 } 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 { Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, Plane, ChevronRight, Plus } from 'lucide-react' 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 { /** Notes pré-chargées côté serveur — hydratées immédiatement sans loading spinner */ 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 }) } 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 if (notebook) { allNotes = allNotes.filter((note: any) => note.notebookId === notebook) } else { allNotes = allNotes.filter((note: any) => !note.notebookId) } // 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)) ) } setNotes(allNotes) 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 => n.notebookId === notebook) } else { filtered = initialNotes.filter(n => !n.notebookId) } setNotes(filtered) 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')) const [showNoteInput, setShowNoteInput] = useState(false) useEffect(() => { setControls({ isTabsMode: notesViewMode === 'tabs', openNoteComposer: () => setShowNoteInput(true), }) return () => setControls(null) }, [notesViewMode, setControls]) const getNotebookIcon = (iconName: string) => { const ICON_MAP: Record = { 'folder': Folder, 'briefcase': Briefcase, 'document': FileText, 'lightning': Zap, 'chart': BarChart3, 'globe': Globe, 'sparkle': Sparkles, 'book': Book, 'heart': Heart, 'crown': Crown, 'music': Music, 'building': Building2, 'flight_takeoff': Plane, } return ICON_MAP[iconName] || Folder } const handleNoteCreatedWrapper = (note: any) => { handleNoteCreated(note) setShowNoteInput(false) } 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 && ( )}
) : (
{!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 && ( )}
)} {showNoteInput && (
)} {isLoading ? (
{t('general.loading')}
) : ( <> setEditingNote({ note, readOnly })} /> {notes.filter((note) => !note.isPinned).length > 0 && (
!note.isPinned)} onEdit={(note, readOnly) => setEditingNote({ note, readOnly })} 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)} /> )}
) }