refactor: split NoteEditor into focused components + consolidate contexts
Phase 1: NoteEditor Split (64KB → 9 focused components) - components/note-editor/: types.ts, context, toolbar, title-block, content-area, metadata-section, full-page, dialog compositions - Maintains backwards compatibility via re-export from note-editor.tsx Phase 2: Context Consolidation (5 → 3 contexts) - NotebooksContext absorbs LabelContext (labels CRUD) - EditorUIContext merges HomeViewContext + NotebookDragContext - Removed: LabelContext, home-view-context, notebook-drag-context Phase 3: React Query Infrastructure - Added QueryProvider with @tanstack/react-query - lib/query-keys.ts: centralized query key definitions - lib/query-hooks.ts: useNotes, useNotebooksQuery, useLabelsQuery - lib/use-refresh.ts: hybrid invalidateQueries + triggerRefresh helper - NotebooksContext: invalidateQueries on mutations (with triggerRefresh fallback) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
146
memento-note/components/note-editor/note-editor-full-page.tsx
Normal file
146
memento-note/components/note-editor/note-editor-full-page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
'use client'
|
||||
|
||||
import { useNoteEditorContext } from './note-editor-context'
|
||||
import { NoteEditorToolbar } from './note-editor-toolbar'
|
||||
import { NoteTitleBlock } from './note-title-block'
|
||||
import { NoteContentArea } from './note-content-area'
|
||||
import { EditorImages } from '@/components/editor-images'
|
||||
import { ComparisonModal } from '@/components/comparison-modal'
|
||||
import { FusionModal } from '@/components/fusion-modal'
|
||||
import { ReminderDialog } from '@/components/reminder-dialog'
|
||||
import { ContextualAIChat } from '@/components/contextual-ai-chat'
|
||||
import { NoteDocumentInfoPanel } from '@/components/note-document-info-panel'
|
||||
import { format } from 'date-fns'
|
||||
import { toast } from 'sonner'
|
||||
import { Note } from '@/lib/types'
|
||||
|
||||
interface NoteEditorFullPageProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
||||
const { state, actions, note, readOnly, notebooks, fileInputRef } = useNoteEditorContext()
|
||||
|
||||
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── outer container ── */}
|
||||
<div className="h-full flex items-stretch overflow-hidden transition-all duration-500">
|
||||
|
||||
{/* ── main scrollable column ── */}
|
||||
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-zinc-950">
|
||||
|
||||
{/* TOOLBAR */}
|
||||
<NoteEditorToolbar mode="fullPage" onClose={onClose} />
|
||||
|
||||
{/* BODY — max-w-4xl, px-12, py-16 */}
|
||||
<div className="max-w-4xl mx-auto w-full px-12 py-16 space-y-12">
|
||||
|
||||
{/* Breadcrumb + Title block */}
|
||||
<div className="space-y-4">
|
||||
{/* Breadcrumb: Notebook › Date */}
|
||||
<div className="flex items-center gap-3 text-[12px] text-foreground/50 uppercase tracking-[.25em] font-bold">
|
||||
{notebookName && <span>{notebookName}</span>}
|
||||
{notebookName && <span>›</span>}
|
||||
<span suppressHydrationWarning>
|
||||
{format(new Date(note.contentUpdatedAt), 'MMM d, yyyy')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<NoteTitleBlock />
|
||||
</div>
|
||||
|
||||
{/* Hero image — show first note image if present */}
|
||||
{state.allImages.length > 0 && (
|
||||
<div className="aspect-[16/9] w-full bg-slate-100 dark:bg-zinc-900 rounded-xl overflow-hidden shadow-xl">
|
||||
<img
|
||||
src={state.allImages[0]}
|
||||
alt={state.title}
|
||||
className="w-full h-full object-cover grayscale contrast-110 hover:grayscale-0 transition-all duration-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content area — max-w-3xl for wider reading column */}
|
||||
<div className="max-w-3xl mx-auto w-full pb-32">
|
||||
<NoteContentArea />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Side panel: AI Chat ── */}
|
||||
{state.aiOpen && (
|
||||
<div className="h-full self-stretch bg-background flex flex-col z-50 shrink-0">
|
||||
<ContextualAIChat
|
||||
onClose={() => actions.setAiOpen(false)}
|
||||
noteTitle={state.title}
|
||||
noteContent={state.content}
|
||||
noteImages={state.allImages}
|
||||
noteId={note.id}
|
||||
onApplyToNote={(nc: string) => {
|
||||
actions.setPreviousContentForCopilot(state.content)
|
||||
actions.setContent(nc)
|
||||
if (state.noteType === 'markdown') actions.setShowMarkdownPreview(true)
|
||||
}}
|
||||
onUndoLastAction={state.previousContentForCopilot !== null ? () => { actions.setContent(state.previousContentForCopilot!); actions.setPreviousContentForCopilot(null) } : undefined}
|
||||
lastActionApplied={state.previousContentForCopilot !== null}
|
||||
notebooks={notebooks}
|
||||
diagramInsertFormat={state.noteType === 'richtext' ? 'html' : 'markdown'}
|
||||
onGenerateTitle={async () => {
|
||||
const plain = state.content.replace(/<[^>]+>/g, ' ').trim()
|
||||
const wordCount = plain.split(/\s+/).filter(Boolean).length
|
||||
if (wordCount < 10) {
|
||||
toast.error('Ajoutez au moins 10 mots avant de générer un titre.')
|
||||
return
|
||||
}
|
||||
actions.setIsProcessingAI(true)
|
||||
try {
|
||||
const res = await fetch('/api/ai/title-suggestions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: plain }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const s = data.suggestions?.[0]?.title ?? ''
|
||||
if (s) {
|
||||
actions.setTitle(s)
|
||||
toast.success('Titre généré !')
|
||||
} else {
|
||||
toast.error('Impossible de générer un titre.')
|
||||
}
|
||||
} else {
|
||||
toast.error('Erreur lors de la génération du titre.')
|
||||
}
|
||||
} catch { toast.error('Erreur réseau.') } finally { actions.setIsProcessingAI(false) }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Side panel: Document Info ── */}
|
||||
{state.infoOpen && (
|
||||
<div className="w-[400px] h-full self-stretch border-l border-black/10 dark:border-white/10 bg-background flex flex-col z-50 shrink-0">
|
||||
<NoteDocumentInfoPanel
|
||||
note={note}
|
||||
content={state.content}
|
||||
onClose={() => actions.setInfoOpen(false)}
|
||||
onNoteRestored={(r: Note) => { actions.setContent(r.content || ''); actions.setTitle(r.title || ''); actions.setIsDirty(false) }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" onChange={actions.handleImageUpload} />
|
||||
<ReminderDialog
|
||||
open={state.showReminderDialog}
|
||||
onOpenChange={actions.setShowReminderDialog}
|
||||
currentReminder={state.currentReminder}
|
||||
onSave={actions.handleReminderSave}
|
||||
onRemove={actions.handleRemoveReminder}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user