write/server/routes/notes.js
chris 37448be5a8 Add notes panel, font picker, sticky toolbar, and prompt improvements
- 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>
2026-05-24 20:21:26 -04:00

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