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:
51
web/src/api/client.ts
Normal file
51
web/src/api/client.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Thin fetch wrappers for the Petal backend. Everything lives under /api, which
|
||||
// Vite proxies to the Go server in dev and the binary serves directly in prod.
|
||||
|
||||
export interface DocSummary {
|
||||
id: string
|
||||
title: string
|
||||
word_count: number
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
id: string
|
||||
user_id: string
|
||||
title: string
|
||||
content: string // Tiptap JSON (stringified)
|
||||
content_text: string // flattened plain text
|
||||
word_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// Fields the editor sends on auto-save. All optional so a rename can send title
|
||||
// alone; the editor sends the full set.
|
||||
export interface DocUpdate {
|
||||
title?: string
|
||||
content?: string
|
||||
content_text?: string
|
||||
word_count?: number
|
||||
}
|
||||
|
||||
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...init,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => '')
|
||||
throw new Error(`${res.status} ${res.statusText}${detail ? `: ${detail}` : ''}`)
|
||||
}
|
||||
if (res.status === 204) return undefined as T
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
export const api = {
|
||||
listDocs: () => req<DocSummary[]>('/docs'),
|
||||
createDoc: () => req<Document>('/docs', { method: 'POST' }),
|
||||
getDoc: (id: string) => req<Document>(`/docs/${id}`),
|
||||
updateDoc: (id: string, body: DocUpdate) =>
|
||||
req<Document>(`/docs/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
deleteDoc: (id: string) => req<void>(`/docs/${id}`, { method: 'DELETE' }),
|
||||
}
|
||||
Reference in New Issue
Block a user