df1a7aebc9
- Add spine--format-defs defconst shared between index and book models - Add spine-book-model with status extraction, format selector, notes, recommendation - Add templates/book.mustache (Pico CSS, journal-style layout) - Add httpd/book handler serving the detail page - Link book titles in index view to /book?id=X - Add spine-book-model-test.el with 13 tests
110 lines
4.4 KiB
Markdown
110 lines
4.4 KiB
Markdown
# Book Detail View Design
|
|
|
|
**Date:** 2026-06-22
|
|
**Status:** Draft
|
|
|
|
## Summary
|
|
|
|
Add a dedicated book detail page (`/book?id=X`) following the journal-style mockup (Direction B). The page shows full book metadata, format badges, reading log, note composer, and recommendation section. Uses Pico CSS and Tabler icons, matching the add-book template style.
|
|
|
|
## Route
|
|
|
|
- **`GET /book?id=<id>`** — renders the full detail page
|
|
- No POST handling (editing flows go through existing `/edit`)
|
|
- Book not found → redirect to `/index`
|
|
|
|
## Model: `spine-book-model`
|
|
|
|
Builds an `ht` (hashtable) view model from one book plist.
|
|
|
|
### Fields
|
|
|
|
| Field | Type | Source | Notes |
|
|
|---|---|---|---|
|
|
| `cover_icon` | string | hardcoded `"ti-book"` | Placeholder cover icon |
|
|
| `title` | string | `:title` minus TODO prefix | Strip leading `WANT `, `READING `, `READ ` |
|
|
| `author` | string | `:author` | Empty string when nil |
|
|
| `status_class` | string | TODO prefix → lowercase | `"want"` / `"reading"` / `"read"` / `""` |
|
|
| `status_label` | string | TODO prefix → capitalized | `"Want"` / `"Reading"` / `"Read"` / `""` |
|
|
| `meta` | string | format + added date | e.g. `"started 20 Feb · audiobook"`. Omits date if not present, format if not present |
|
|
| `progress_label` | string | — | Empty string; reserved for future progress tracking |
|
|
| `formats` | array | all 3 known formats | `[{icon, label, active}]`. Active matches `:format` property |
|
|
| `notes` | array | `:notes` | `[{date, text}]`. Empty array when no notes |
|
|
| `recommendation` | object or nil | `:rec_by` + `:rec_note` | `{initials, by, note}`. nil when no rec_by |
|
|
|
|
### Status extraction
|
|
|
|
Book headlines have TODO keyword prefixes (`WANT`, `READING`, `READ`) as part of their title text. The model function:
|
|
|
|
1. Strips the prefix from the title for display
|
|
2. Maps recognized prefixes to status classes
|
|
|
|
### Format list
|
|
|
|
Hardcoded list of three known formats:
|
|
- `hardcover` → `ti-book` → "Hardcover"
|
|
- `ebook` → `ti-device-tablet` → "eBook"
|
|
- `audiobook` → `ti-headphones` → "Audiobook"
|
|
|
|
The book's `:format` property determines which is `active: true`; the others are `active: false`.
|
|
|
|
### Recommendation
|
|
|
|
Generated from `:rec_by` (name) and `:rec_note` (text). No date stored currently.
|
|
|
|
`initials` derived from the name: uppercase first letter of each space-separated part. e.g. "Priya" → "P", "John Doe" → "JD".
|
|
|
|
## Template: `templates/book.mustache`
|
|
|
|
Based on `spine-mockup-b-journal.mustache` with adaptations:
|
|
- Pico CSS framework
|
|
- Tabler icons for icons
|
|
- Progress-related text omitted from the composer placeholder (says "Add a note…" without percentage)
|
|
- Back link at top to return to index with this book selected (`/index?id=X`)
|
|
- No recommendation date field (not stored in data model)
|
|
- Conditional rendering of recommendation section
|
|
|
|
## Handler: `httpd/book`
|
|
|
|
Registered as `httpd/book` matching the existing add/edit handler pattern:
|
|
|
|
```elisp
|
|
(defun httpd/book (proc uri-path query request)
|
|
"Show the journal-style detail page for a single book."
|
|
(let* ((id (cadr (assoc "id" query)))
|
|
(books (spine-books))
|
|
(book (cl-find id books :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
|
(if book
|
|
(insert (spine-render "book.mustache" (spine-book-model book)))
|
|
(httpd-redirect proc "/index" 302))))
|
|
```
|
|
| Action | File | Description |
|
|
|---|---|---|
|
|
| New | `templates/book.mustache` | Pico CSS + Tabler detail page |
|
|
| Modify | `spine.el` | Add `spine-book-model`, `httpd/book` handler, title link in index model |
|
|
| New | `test/spine-book-model-test.el` | Tests for `spine-book-model` |
|
|
| Modify | `test/spine-index-model-test.el` | Tests for title link in index model |
|
|
|
|
## Edge cases
|
|
|
|
- **No TODO prefix on headline** → title shown as-is, status empty
|
|
- **Unknown TODO prefix** (not WANT/READING/READ) → title shown with prefix, status empty
|
|
- **Book not found** → redirect to `/index`
|
|
- **No format property** → no format active in the selector
|
|
- **No author** → empty string
|
|
- **No notes** → empty reading log section
|
|
- **No recommendation** → section not rendered (optional block)
|
|
|
|
## Testing
|
|
|
|
Test file `test/spine-book-model-test.el` covers:
|
|
- Title stripped of TODO prefix
|
|
- Status class derived correctly for WANT/READING/READ
|
|
- Meta string from format + added date
|
|
- Format list with correct active flag
|
|
- Notes mapping
|
|
- Recommendation with initials
|
|
- No recommendation when rec_by absent
|
|
- Book with no TODO prefix (handles gracefully)
|
|
- Book with no format or date (meta is empty)
|