feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,15 +4,21 @@ import React, { useState, useEffect, useCallback, useRef, useTransition, useMemo
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Note } from '@/lib/types'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote } from '@/app/actions/notes'
|
||||
import { NotesEditorialView } from '@/components/notes-editorial-view'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote, deleteNote, togglePin, toggleArchive, updateNote, updateFullOrderWithoutRevalidation } from '@/app/actions/notes'
|
||||
import { NotesListViews, type NotesLayoutMode, type NotesViewType } from '@/components/notes-list-views'
|
||||
import {
|
||||
NOTES_LAYOUT_STORAGE_KEY,
|
||||
NOTES_VIEW_TYPE_STORAGE_KEY,
|
||||
parseNotesLayoutMode,
|
||||
parseNotesViewType,
|
||||
setNotesLayoutPreference,
|
||||
setNotesViewTypePreference,
|
||||
} from '@/lib/notes-view-preference'
|
||||
|
||||
import { MemoryEchoNotification } from '@/components/memory-echo-notification'
|
||||
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 } from 'lucide-react'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useRefresh } from '@/lib/use-refresh'
|
||||
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, Tag as TagIcon, X, Menu, LayoutGrid, List, Table } 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'
|
||||
@@ -58,9 +64,16 @@ type InitialSettings = {
|
||||
interface HomeClientProps {
|
||||
initialNotes: Note[]
|
||||
initialSettings: InitialSettings
|
||||
initialLayoutMode?: NotesLayoutMode
|
||||
initialViewType?: NotesViewType
|
||||
}
|
||||
|
||||
export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
export function HomeClient({
|
||||
initialNotes,
|
||||
initialSettings,
|
||||
initialLayoutMode = 'list',
|
||||
initialViewType = 'notes',
|
||||
}: HomeClientProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const { t } = useLanguage()
|
||||
@@ -86,9 +99,11 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const notesRef = useRef(notes)
|
||||
notesRef.current = notes
|
||||
const { refreshKey, triggerRefresh } = useNoteRefresh()
|
||||
const { refreshNotes } = useRefresh()
|
||||
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()
|
||||
@@ -99,6 +114,41 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([])
|
||||
const [isTagsExpanded, setIsTagsExpanded] = useState(false)
|
||||
const [tagSearchQuery, setTagSearchQuery] = useState('')
|
||||
const [viewType, setViewType] = useState<NotesViewType>(initialViewType)
|
||||
const [layoutMode, setLayoutMode] = useState<NotesLayoutMode>(initialLayoutMode)
|
||||
|
||||
useEffect(() => {
|
||||
const storedLayout = parseNotesLayoutMode(localStorage.getItem(NOTES_LAYOUT_STORAGE_KEY))
|
||||
const storedViewType = parseNotesViewType(localStorage.getItem(NOTES_VIEW_TYPE_STORAGE_KEY))
|
||||
if (storedLayout !== initialLayoutMode) {
|
||||
setLayoutMode(storedLayout)
|
||||
setNotesLayoutPreference(storedLayout)
|
||||
}
|
||||
if (storedViewType !== initialViewType) {
|
||||
setViewType(storedViewType)
|
||||
setNotesViewTypePreference(storedViewType)
|
||||
}
|
||||
}, [initialLayoutMode, initialViewType])
|
||||
|
||||
useEffect(() => {
|
||||
setNotesViewTypePreference(viewType)
|
||||
}, [viewType])
|
||||
|
||||
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') {
|
||||
setLayoutMode(detail)
|
||||
setViewType('notes')
|
||||
}
|
||||
}
|
||||
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(() => {
|
||||
@@ -112,12 +162,86 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
}, [searchParams, router])
|
||||
|
||||
const notebookFilter = searchParams.get('notebook')
|
||||
|
||||
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')
|
||||
const search = searchParams.get('search')?.trim() || null
|
||||
|
||||
if (notebookFilter && note.notebookId !== notebookFilter) return prevNotes
|
||||
if (!notebookFilter && note.notebookId) return prevNotes
|
||||
@@ -135,11 +259,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
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)
|
||||
@@ -150,6 +269,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
return [...pinnedNotes, note, ...unpinnedNotes]
|
||||
}
|
||||
})
|
||||
emitNoteChange({ type: 'created', note })
|
||||
|
||||
if (!note.notebookId) {
|
||||
const wordCount = (note.content || '').trim().split(/\s+/).filter(w => w.length > 0).length
|
||||
@@ -157,7 +277,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
setNotebookSuggestion({ noteId: note.id, content: note.content || '' })
|
||||
}
|
||||
}
|
||||
}, [searchParams, labels, router])
|
||||
}, [searchParams, labels, fetchNotesForCurrentView])
|
||||
|
||||
// Always fetch fresh from server — avoids stale state after a save regardless of
|
||||
// whether the notes list has re-fetched yet.
|
||||
@@ -210,7 +330,134 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
setPinnedNotes(prev => prev.map(n => n.id === noteId ? { ...n, size } : n))
|
||||
}, [])
|
||||
|
||||
useReminderCheck(notes)
|
||||
const patchNoteInList = useCallback((noteId: string, patch: Partial<Note>) => {
|
||||
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<Note, 'notebookId' | '_isShared'>) => {
|
||||
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<Note>) => {
|
||||
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 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(() => {
|
||||
@@ -248,86 +495,27 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
return () => window.removeEventListener('label-deleted', handler)
|
||||
}, [])
|
||||
|
||||
const prevRefreshKey = useRef(refreshKey)
|
||||
|
||||
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)
|
||||
|
||||
const isBackgroundRefresh = refreshKey > prevRefreshKey.current
|
||||
prevRefreshKey.current = refreshKey
|
||||
|
||||
const hasActiveFilter = search || labelFilter.length > 0 || colorFilter
|
||||
|
||||
const load = async () => {
|
||||
if (!isBackgroundRefresh) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
let allNotes = search
|
||||
? await searchNotes(search, true, notebook || undefined)
|
||||
: await getAllNotes(false, notebook || undefined)
|
||||
|
||||
const sharedOnly = searchParams.get('shared') === '1'
|
||||
const remindersOnly = searchParams.get('reminders') === '1'
|
||||
|
||||
if (sharedOnly) {
|
||||
allNotes = allNotes.filter((note: any) => note._isShared)
|
||||
} else if (remindersOnly) {
|
||||
allNotes = allNotes.filter((note: any) => note.reminder !== null)
|
||||
} else if (!notebook && !search) {
|
||||
allNotes = allNotes.filter((note: any) => !note.notebookId && !note._isShared)
|
||||
}
|
||||
|
||||
if (labelFilter.length > 0) {
|
||||
allNotes = allNotes.filter((note: any) =>
|
||||
labelFilter.every((label: string) => note.labels?.includes(label))
|
||||
)
|
||||
}
|
||||
|
||||
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(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)
|
||||
if (hasActiveFilter || notebook) {
|
||||
void fetchNotesForCurrentView()
|
||||
return
|
||||
}
|
||||
|
||||
if (refreshKey > 0 || hasActiveFilter) {
|
||||
const cancelled = { value: false }
|
||||
load().then(() => { if (cancelled.value) return })
|
||||
return () => { cancelled.value = true }
|
||||
} else {
|
||||
let filtered = initialNotes
|
||||
const sharedOnly = searchParams.get('shared') === '1'
|
||||
const remindersOnly = searchParams.get('reminders') === '1'
|
||||
if (notebook) {
|
||||
filtered = initialNotes.filter((n: any) => n.notebookId === notebook && !n._isShared)
|
||||
} else if (sharedOnly) {
|
||||
filtered = initialNotes.filter((n: any) => n._isShared)
|
||||
} else if (remindersOnly) {
|
||||
filtered = initialNotes.filter((n: any) => n.reminder !== null)
|
||||
} else {
|
||||
filtered = initialNotes.filter((n: any) => !n.notebookId && !n._isShared)
|
||||
}
|
||||
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, refreshKey])
|
||||
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'))
|
||||
|
||||
@@ -437,19 +625,13 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
params.delete('openNote')
|
||||
const qs = params.toString()
|
||||
router.replace(qs ? `/home?${qs}` : '/home', { scroll: false })
|
||||
// Invalidate notes cache and trigger refresh
|
||||
refreshNotes(searchParams.get('notebook') || null)
|
||||
}, [refreshNotes, router, searchParams])
|
||||
}, [router, searchParams])
|
||||
|
||||
// Called by NoteEditor when a save succeeds — update local state immediately
|
||||
// so the user sees fresh data if they reopen the note before getAllNotes() completes
|
||||
const handleNoteSaved = useCallback((savedNote: Note) => {
|
||||
setNotes(prev => prev.map(n => n.id === savedNote.id ? { ...n, ...savedNote } : n))
|
||||
setPinnedNotes(prev => prev.map(n => n.id === savedNote.id ? { ...n, ...savedNote } : n))
|
||||
setEditingNote(prev => prev?.note.id === savedNote.id ? { ...prev, note: savedNote } : prev)
|
||||
// Refresh sidebar note titles so the new title appears immediately
|
||||
refreshNotes(savedNote.notebookId || null)
|
||||
}, [refreshNotes])
|
||||
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 (
|
||||
<div
|
||||
@@ -626,7 +808,78 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="bg-foreground/[0.03] dark:bg-white/[0.04] p-0.5 rounded-full flex border border-border/30">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewType('notes')}
|
||||
className={cn(
|
||||
'px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all',
|
||||
viewType === 'notes'
|
||||
? 'bg-foreground text-background shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{t('notes.viewNotes')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewType('tasks')}
|
||||
className={cn(
|
||||
'px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all',
|
||||
viewType === 'tasks'
|
||||
? 'bg-foreground text-background shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{t('notes.viewTasks')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{viewType === 'notes' && (
|
||||
<div className="bg-foreground/[0.03] dark:bg-white/[0.04] p-0.5 rounded-full flex border border-border/30 items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLayoutMode('grid')}
|
||||
className={cn(
|
||||
'p-1.5 rounded-full transition-all',
|
||||
layoutMode === 'grid'
|
||||
? 'bg-foreground text-background shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
title={t('notes.layoutGridTitle')}
|
||||
>
|
||||
<LayoutGrid size={13} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLayoutMode('list')}
|
||||
className={cn(
|
||||
'p-1.5 rounded-full transition-all',
|
||||
layoutMode === 'list'
|
||||
? 'bg-foreground text-background shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
title={t('notes.layoutListTitle')}
|
||||
>
|
||||
<List size={13} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLayoutMode('table')}
|
||||
className={cn(
|
||||
'p-1.5 rounded-full transition-all',
|
||||
layoutMode === 'table'
|
||||
? 'bg-foreground text-background shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
title={t('notes.layoutTableTitle')}
|
||||
>
|
||||
<Table size={13} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchParams.get('notebook') && (
|
||||
<button
|
||||
onClick={() => setSummaryDialogOpen(true)}
|
||||
@@ -733,46 +986,36 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
<div className="px-4 sm:px-8 md:px-12 flex-1 pb-10 sm:pb-16 md:pb-20">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">{t('general.loading')}</div>
|
||||
) : (
|
||||
<div className="max-w-3xl space-y-16">
|
||||
{sortedPinnedNotes.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-2">
|
||||
{t('notes.pinned')}
|
||||
</h2>
|
||||
<NotesEditorialView
|
||||
notes={sortedPinnedNotes}
|
||||
onOpen={(note: Note, readOnly?: boolean) => handleOpenNoteFresh(note.id, readOnly ?? false)}
|
||||
notebookName={currentNotebook?.name}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sortedNotes.filter((note) => !note.isPinned).length > 0 && (
|
||||
<NotesEditorialView
|
||||
notes={sortedNotes.filter((note) => !note.isPinned)}
|
||||
onOpen={(note, readOnly) => handleOpenNoteFresh(note.id, readOnly ?? false)}
|
||||
notebookName={currentNotebook?.name}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
/>
|
||||
)}
|
||||
|
||||
{notes.length === 0 && (
|
||||
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
|
||||
<p className="font-memento-serif text-xl italic text-muted-foreground">
|
||||
{t('notes.emptyState')}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleAddNote}
|
||||
disabled={isCreating}
|
||||
className="px-6 py-2 border border-foreground text-[13px] uppercase tracking-[0.2em] hover:bg-foreground hover:text-background transition-all"
|
||||
>
|
||||
{t('notes.createFirst')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
) : notes.length === 0 ? (
|
||||
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
|
||||
<p className="font-memento-serif text-xl italic text-muted-foreground">
|
||||
{t('notes.emptyState')}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleAddNote}
|
||||
disabled={isCreating}
|
||||
className="px-6 py-2 border border-foreground text-[13px] uppercase tracking-[0.2em] hover:bg-foreground hover:text-background transition-all"
|
||||
>
|
||||
{t('notes.createFirst')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<NotesListViews
|
||||
notes={sortedNotes}
|
||||
pinnedNotes={sortedPinnedNotes}
|
||||
viewType={viewType}
|
||||
layoutMode={layoutMode}
|
||||
onOpen={(note, readOnly) => handleOpenNoteFresh(note.id, readOnly ?? false)}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
notebookName={currentNotebook?.name}
|
||||
onTogglePin={handleTogglePin}
|
||||
onDeleteNote={handleDeleteNoteFromList}
|
||||
onArchiveNote={handleArchiveNoteFromList}
|
||||
onMoveToNotebook={handleMoveNoteToNotebook}
|
||||
onNotePatch={handleNoteContentPatch}
|
||||
onNoteIllustrationGenerated={handleNoteIllustrationGenerated}
|
||||
onGridReorder={handleGridReorder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -784,8 +1027,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MemoryEchoNotification onOpenNote={(noteId) => handleOpenNoteFresh(noteId)} />
|
||||
|
||||
{notebookSuggestion && (
|
||||
<NotebookSuggestionToast
|
||||
noteId={notebookSuggestion.noteId}
|
||||
@@ -798,7 +1039,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
<BatchOrganizationDialog
|
||||
open={batchOrganizationOpen}
|
||||
onOpenChange={setBatchOrganizationOpen}
|
||||
onNotesMoved={() => router.refresh()}
|
||||
onNotesMoved={() => fetchNotesForCurrentView({ silent: true })}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -810,7 +1051,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
if (!open) dismissLabelSuggestion()
|
||||
}}
|
||||
notebookId={suggestNotebookId}
|
||||
onLabelsCreated={() => router.refresh()}
|
||||
onLabelsCreated={() => fetchNotesForCurrentView({ silent: true })}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -846,8 +1087,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
notebookName={currentNotebook.name}
|
||||
onDone={() => {
|
||||
refreshNotebooks()
|
||||
triggerRefresh()
|
||||
router.refresh()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user