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:
47
web/src/components/StatusBar/StatusBar.tsx
Normal file
47
web/src/components/StatusBar/StatusBar.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { SaveStatus } from '../../hooks/useAutoSave'
|
||||
|
||||
interface Props {
|
||||
wordCount: number
|
||||
saveStatus: SaveStatus
|
||||
}
|
||||
|
||||
const SAVE_LABEL: Record<SaveStatus, string> = {
|
||||
idle: '',
|
||||
pending: 'Editing…',
|
||||
saving: 'Saving…',
|
||||
saved: 'Saved just now',
|
||||
error: "Couldn't save",
|
||||
}
|
||||
|
||||
// StatusBar is the slim footer: word count on the left, save state on the right.
|
||||
// The checkpoint dot (grammar pass) joins it in Phase 3.
|
||||
export function StatusBar({ wordCount, saveStatus }: Props) {
|
||||
const label = SAVE_LABEL[saveStatus]
|
||||
return (
|
||||
<footer
|
||||
className="flex h-9 shrink-0 items-center gap-3 px-6 text-xs"
|
||||
style={{ borderTop: '1px solid var(--color-border)', color: 'var(--color-muted)' }}
|
||||
>
|
||||
<span>
|
||||
{wordCount} {wordCount === 1 ? 'word' : 'words'}
|
||||
</span>
|
||||
{label && (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5"
|
||||
style={{ color: saveStatus === 'error' ? 'var(--color-accent)' : undefined }}
|
||||
>
|
||||
{saveStatus === 'saved' && (
|
||||
<span
|
||||
className="inline-block h-1.5 w-1.5 rounded-full"
|
||||
style={{ background: 'var(--color-success)' }}
|
||||
/>
|
||||
)}
|
||||
{label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user