feat: add logos editor
This commit is contained in:
@@ -1,15 +1,44 @@
|
||||
/**
|
||||
* Logos page: default view for a recollection. Placeholder for now.
|
||||
* Logos page: BlockNote editor for the recollection. Content persisted in recollection store.
|
||||
* Layout and styling aligned with Flux (same flex/overflow, bg-background, theme).
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
import { useCreateBlockNote } from '@blocknote/react'
|
||||
import { BlockNoteView } from '@blocknote/shadcn'
|
||||
import { useTheme } from '@/lib/themeContext'
|
||||
import { useRecollectionMenubar } from '../RecollectionMenubarContext'
|
||||
import { getLogosContent, setLogosContent, type StoredLogosContent } from '../recollectionStore'
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 400
|
||||
|
||||
export function LogosPage() {
|
||||
const { pathname } = useLocation()
|
||||
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||
const { theme } = useTheme()
|
||||
const { setCustomContent } = useRecollectionMenubar()
|
||||
const isLogosActive = pathname.endsWith('/logos') || pathname.match(/\/recollections\/[^/]+\/?$/)
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const initialContent = useMemo(
|
||||
() => (recollectionId ? getLogosContent(recollectionId) ?? undefined : undefined),
|
||||
[recollectionId]
|
||||
)
|
||||
|
||||
const editor = useCreateBlockNote({ initialContent }, [recollectionId])
|
||||
|
||||
const persistContent = useCallback(() => {
|
||||
if (!recollectionId || !editor) return
|
||||
try {
|
||||
const doc = editor.document
|
||||
const serialized = JSON.parse(JSON.stringify(doc)) as StoredLogosContent
|
||||
setLogosContent(recollectionId, serialized)
|
||||
} catch {
|
||||
// ignore serialize errors
|
||||
}
|
||||
}, [recollectionId, editor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLogosActive) return
|
||||
@@ -17,10 +46,69 @@ export function LogosPage() {
|
||||
return () => setCustomContent(null)
|
||||
}, [isLogosActive, setCustomContent])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !recollectionId) return
|
||||
const handleChange = () => {
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
saveTimeoutRef.current = null
|
||||
persistContent()
|
||||
}, SAVE_DEBOUNCE_MS)
|
||||
}
|
||||
editor.onChange(handleChange)
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
saveTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [editor, recollectionId, persistContent])
|
||||
|
||||
if (!recollectionId) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col items-center justify-center p-8 text-muted-foreground">
|
||||
<p className="text-sm">No recollection selected.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col items-center justify-center p-8 text-muted-foreground">
|
||||
<p className="text-sm">Loading…</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
|
||||
const el = scrollContainerRef.current
|
||||
if (!el) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = el
|
||||
const canScrollUp = scrollTop > 0
|
||||
const canScrollDown = scrollTop < scrollHeight - clientHeight
|
||||
const scrollingDown = e.deltaY > 0
|
||||
const scrollingUp = e.deltaY < 0
|
||||
if ((scrollingDown && canScrollDown) || (scrollingUp && canScrollUp)) {
|
||||
e.preventDefault()
|
||||
el.scrollBy({ top: e.deltaY, behavior: 'auto' })
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col items-center justify-center p-8 text-muted-foreground">
|
||||
<p className="text-sm">Logos</p>
|
||||
<p className="mt-1 text-xs">This page is empty for now.</p>
|
||||
<div className="logos-page flex h-full min-h-0 flex-1 flex-col overflow-hidden bg-background">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden"
|
||||
onWheelCapture={handleWheel}
|
||||
>
|
||||
<div className="logos-editor bn-shadcn mx-auto w-full max-w-3xl px-6 py-8">
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
theme={theme}
|
||||
className="min-h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user