- Notes: per-story rich-text notes panel (right drawer) with TipTap editor, image support, autosave, and full CRUD API - Font picker: 15 Google Fonts selectable from a floating Aa button, persisted to localStorage via --font-body CSS variable - Sticky toolbar: pulled formatting bar out of overflow:hidden wrapper so it sticks below the topbar while scrolling - Prompts: 100 additional built-in prompts (120 total) in a shuffled no-repeat queue; pre-fetch on page load so the AI has time to respond; timeout raised to 45s; error logging + /api/prompts/test debug endpoint; source badge shows whether prompt came from AI or built-in list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
68 lines
2.7 KiB
JavaScript
68 lines
2.7 KiB
JavaScript
import { Router } from 'express'
|
|
import db from '../db.js'
|
|
import { auth } from '../middleware/auth.js'
|
|
|
|
// mergeParams: true is required so req.params.storyId is visible
|
|
// inside this child router (Express strips parent params by default)
|
|
const router = Router({ mergeParams: true })
|
|
router.use(auth)
|
|
|
|
const parse = s => { try { return JSON.parse(s || '{}') } catch { return {} } }
|
|
const safe = s => JSON.stringify(s ?? {})
|
|
|
|
// Verify the story belongs to the requesting user
|
|
function getStory(storyId, userId) {
|
|
return db.prepare('SELECT id FROM stories WHERE id = ? AND user_id = ?').get(storyId, userId)
|
|
}
|
|
|
|
// GET /api/stories/:storyId/notes
|
|
router.get('/', (req, res) => {
|
|
if (!getStory(req.params.storyId, req.user.id)) return res.status(404).json({ error: 'Not found' })
|
|
const rows = db.prepare(
|
|
'SELECT * FROM notes WHERE story_id = ? AND user_id = ? ORDER BY updated_at DESC'
|
|
).all(req.params.storyId, req.user.id)
|
|
res.json(rows.map(r => ({ ...r, content: parse(r.content) })))
|
|
})
|
|
|
|
// POST /api/stories/:storyId/notes
|
|
router.post('/', (req, res) => {
|
|
if (!getStory(req.params.storyId, req.user.id)) return res.status(404).json({ error: 'Not found' })
|
|
const { title = 'Untitled Note', content = {} } = req.body || {}
|
|
const { lastInsertRowid } = db.prepare(
|
|
'INSERT INTO notes (story_id, user_id, title, content) VALUES (?, ?, ?, ?)'
|
|
).run(req.params.storyId, req.user.id, title, safe(content))
|
|
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(lastInsertRowid)
|
|
res.json({ ...note, content: parse(note.content) })
|
|
})
|
|
|
|
// PUT /api/stories/:storyId/notes/:noteId
|
|
router.put('/:noteId', (req, res) => {
|
|
const note = db.prepare(
|
|
'SELECT * FROM notes WHERE id = ? AND story_id = ? AND user_id = ?'
|
|
).get(req.params.noteId, req.params.storyId, req.user.id)
|
|
if (!note) return res.status(404).json({ error: 'Not found' })
|
|
|
|
const { title, content } = req.body || {}
|
|
db.prepare(
|
|
'UPDATE notes SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
|
).run(
|
|
title !== undefined ? title : note.title,
|
|
content !== undefined ? safe(content) : note.content,
|
|
note.id
|
|
)
|
|
const updated = db.prepare('SELECT * FROM notes WHERE id = ?').get(note.id)
|
|
res.json({ ...updated, content: parse(updated.content) })
|
|
})
|
|
|
|
// DELETE /api/stories/:storyId/notes/:noteId
|
|
router.delete('/:noteId', (req, res) => {
|
|
const note = db.prepare(
|
|
'SELECT * FROM notes WHERE id = ? AND story_id = ? AND user_id = ?'
|
|
).get(req.params.noteId, req.params.storyId, req.user.id)
|
|
if (!note) return res.status(404).json({ error: 'Not found' })
|
|
db.prepare('DELETE FROM notes WHERE id = ?').run(note.id)
|
|
res.json({ ok: true })
|
|
})
|
|
|
|
export default router
|