Phase 2: document CRUD + auto-save
Backend internal/docs: chi sub-router (list/create/get/update/delete) mounted at /api/docs, scoped to the local user. Create uses RETURNING; update is a COALESCE partial-update so rename and full editor save share one PUT. JSON 404/400 errors; handlers_test.go walks the lifecycle. Frontend: api/client.ts, useAutoSave (1.5s debounce + saveNow flush before doc switch), EditorCore (Tiptap StarterKit/Underline/TextAlign/ Placeholder/CharacterCount) + Toolbar, DocList/DocListItem, StatusBar, and an App.tsx that orchestrates load/select/create/delete with optimistic sidebar patching. content + content_text + word_count are emitted together on every edit. .petal-prose styling (Lora body, Nunito headings). Verified: tsc clean, vite build, go build, full CRUD smoke test incl. CJK title round-trip and SPA serve. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
66
web/src/hooks/useAutoSave.ts
Normal file
66
web/src/hooks/useAutoSave.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { api, type DocUpdate } from '../api/client'
|
||||
|
||||
export type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'
|
||||
|
||||
const DEBOUNCE_MS = 1500
|
||||
const SAVED_FADE_MS = 3000
|
||||
|
||||
// useAutoSave debounces document saves. Call schedule() on every edit; it fires
|
||||
// PUT /api/docs/:id 1.5s after the last change. status drives the StatusBar:
|
||||
// pending → saving → saved (fades to idle after 3s).
|
||||
export function useAutoSave(docId: string | null) {
|
||||
const [status, setStatus] = useState<SaveStatus>('idle')
|
||||
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const fadeRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const pendingRef = useRef<DocUpdate | null>(null)
|
||||
// Latest doc id, read inside the timer so a doc switch doesn't save to the old one.
|
||||
const docIdRef = useRef(docId)
|
||||
docIdRef.current = docId
|
||||
|
||||
const flush = useCallback(async () => {
|
||||
const id = docIdRef.current
|
||||
const body = pendingRef.current
|
||||
pendingRef.current = null
|
||||
if (!id || !body) return
|
||||
|
||||
setStatus('saving')
|
||||
try {
|
||||
await api.updateDoc(id, body)
|
||||
setStatus('saved')
|
||||
clearTimeout(fadeRef.current)
|
||||
fadeRef.current = setTimeout(() => setStatus('idle'), SAVED_FADE_MS)
|
||||
} catch (err) {
|
||||
console.error('auto-save failed', err)
|
||||
setStatus('error')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const schedule = useCallback(
|
||||
(update: DocUpdate) => {
|
||||
pendingRef.current = { ...pendingRef.current, ...update }
|
||||
setStatus('pending')
|
||||
clearTimeout(fadeRef.current)
|
||||
clearTimeout(debounceRef.current)
|
||||
debounceRef.current = setTimeout(flush, DEBOUNCE_MS)
|
||||
},
|
||||
[flush],
|
||||
)
|
||||
|
||||
// Save any pending edits immediately (e.g. before switching documents).
|
||||
const saveNow = useCallback(() => {
|
||||
clearTimeout(debounceRef.current)
|
||||
return flush()
|
||||
}, [flush])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
clearTimeout(debounceRef.current)
|
||||
clearTimeout(fadeRef.current)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
return { status, schedule, saveNow }
|
||||
}
|
||||
Reference in New Issue
Block a user