docs: add add-book form design spec
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
# Spine — Add Book Form
|
||||
|
||||
Adds a form to create new books in `spine.org`, plus the write function
|
||||
to persist them.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
"Add book" link (index page)
|
||||
│
|
||||
GET /add
|
||||
│
|
||||
templates/add.mustache ←── [spine-index-model] for datalist values
|
||||
│
|
||||
POST /add (form submission)
|
||||
│
|
||||
[spine-add-book] ──→ writes to spine.org
|
||||
│
|
||||
redirect to /index
|
||||
```
|
||||
|
||||
## Template: `templates/add.mustache`
|
||||
|
||||
A new Mustache template with the Pico CSS + Tabler Icons shell (matching the
|
||||
index page) and a `<form method="post" action="/add">`.
|
||||
|
||||
**View model:**
|
||||
|
||||
```json
|
||||
{
|
||||
"app_title": "spine",
|
||||
"existing_authors": ["Ursula K. Le Guin", "Frank Herbert", ...],
|
||||
"existing_categories": ["scifi", "literary", ...]
|
||||
}
|
||||
```
|
||||
|
||||
`existing_authors` and `existing_categories` are extracted from the current
|
||||
`spine-books` results. Empty lists are fine — the `<datalist>` just has no
|
||||
options.
|
||||
|
||||
**Form fields:**
|
||||
|
||||
| Field | Input type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| date_added | `<input type="date">` | no | defaults to today |
|
||||
| title | `<input type="text">` | **yes** | |
|
||||
| author | `<input type="text" list="authors">` | no | datalist from existing |
|
||||
| category | `<input type="text" list="categories">` | no | datalist from existing tags |
|
||||
| format | `<select>` | no | hardcover / ebook / audiobook / — |
|
||||
| isbn | `<input type="text">` | no | |
|
||||
| cover | `<input type="text">` | no | path relative to spine.org |
|
||||
| rec_by | `<input type="text">` | no | recommender name |
|
||||
| rec_note | `<input type="text">` | no | why it's on your radar |
|
||||
| initial_note | `<textarea>` | no | becomes first reading note |
|
||||
|
||||
**Datalists** for author and category:
|
||||
|
||||
```html
|
||||
<input type="text" name="author" list="authors-list" />
|
||||
<datalist id="authors-list">
|
||||
{{#existing_authors}}<option value="{{.}}">{{/existing_authors}}
|
||||
</datalist>
|
||||
```
|
||||
|
||||
"Add book" link on the index page (in `templates/index.mustache`) — placed in
|
||||
the `<header>` alongside the title:
|
||||
|
||||
```html
|
||||
<a role="button" href="/add">Add book</a>
|
||||
```
|
||||
|
||||
## Write function: `spine-add-book`
|
||||
|
||||
```elisp
|
||||
(spine-add-book &key title author category format isbn cover
|
||||
rec_by rec_note date_added initial_note)
|
||||
→ t on success, signals error on failure
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
1. Visits `spine-org-file` in a temporary buffer
|
||||
2. Inserts a new top-level headline with TODO keyword `WANT`
|
||||
3. Sets properties: `:AUTHOR:`, `:FORMAT:`, `:ISBN:`, `:COVER:`, `:REC_BY:`, `:REC_NOTE:`, `:ADDED:`, `:ID:`
|
||||
4. Sets tags if category provided
|
||||
5. Appends initial note as `- [DATE] note text` if provided
|
||||
6. Saves the buffer
|
||||
7. Returns `t`
|
||||
|
||||
**`:ID:` generation:** short random alphanumeric, e.g. `(format "%04x-%04x" (random 65535) (random 65535))`.
|
||||
|
||||
**`:ADDED:` date:** if `date_added` is provided, format as `[YYYY-MM-DD]`. If nil, omit the property.
|
||||
|
||||
**Properties with nil/empty values are omitted** — only set properties that have non-empty values. This keeps the Org file clean.
|
||||
|
||||
## Handler: `/add`
|
||||
|
||||
```elisp
|
||||
(defservlet add text/html (path query request)
|
||||
(let ((method (car (car request))))
|
||||
(if (equal method "POST")
|
||||
;; Process form submission
|
||||
(progn
|
||||
(spine-add-book
|
||||
:title (cdr (assoc "title" query))
|
||||
:author (cdr (assoc "author" query))
|
||||
:category (cdr (assoc "category" query))
|
||||
:format (cdr (assoc "format" query))
|
||||
:isbn (cdr (assoc "isbn" query))
|
||||
:cover (cdr (assoc "cover" query))
|
||||
:rec_by (cdr (assoc "rec_by" query))
|
||||
:rec_note (cdr (assoc "rec_note" query))
|
||||
:date_added (cdr (assoc "date_added" query))
|
||||
:initial_note (cdr (assoc "initial_note" query)))
|
||||
(httpd-redirect t "/index" 303))
|
||||
;; GET: show the form
|
||||
(let ((model (spine-add-form-model (spine-books))))
|
||||
(insert (spine-render "add.mustache" model))))))
|
||||
```
|
||||
|
||||
`spine-add-form-model` builds the view model from `spine-books` — extracts
|
||||
unique authors and tags into flat lists.
|
||||
|
||||
**Form GET access:** `http://localhost:8080/add`
|
||||
|
||||
**Empty-books case:** if `spine-books` returns nil, the datalists are empty —
|
||||
the form still works, you just get no suggestions.
|
||||
|
||||
## Error handling
|
||||
|
||||
| Case | Behavior |
|
||||
|---|---|
|
||||
| Title missing | redirect to `/add` with error message (query param) |
|
||||
| Write fails | `spine-add-book` signals error, handler catches and shows error page |
|
||||
| `spine.org` missing | `spine-add-book` creates a new file with the `#+TODO` header and first entry |
|
||||
|
||||
## Files changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `templates/add.mustache` | New — add-book form template |
|
||||
| `templates/index.mustache` | Modified — add "Add book" link in header |
|
||||
| `spine.el` | New functions: `spine-add-book`, `spine-add-form-model`. New handler: `/add`. |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Validation beyond title required
|
||||
- Edit existing book
|
||||
- Delete book
|
||||
- Cover image upload (text path only)
|
||||
- Multi-category input (single category string → single tag for now)
|
||||
- Error display on the form page (redirect on error, no inline messages)
|
||||
|
||||
## Styling
|
||||
|
||||
Uses Pico CSS form styles (`<input>`, `<select>`, `<textarea>`) which are
|
||||
styled automatically by the Pico framework. No additional custom CSS needed
|
||||
for the form — Pico handles layout, spacing, and theme.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] "Add book" link visible on index page
|
||||
- [ ] `GET /add` shows form with Pico styling
|
||||
- [ ] Author and category fields have datalist suggestions from existing data
|
||||
- [ ] Submitting the form creates a new `* WANT Title` headline in `spine.org`
|
||||
- [ ] Properties are set correctly (title, author, format, ISBN, etc.)
|
||||
- [ ] Initial note appears as `- [YYYY-MM-DD] text` in the body
|
||||
- [ ] Empty optional fields are omitted (no empty properties)
|
||||
- [ ] After submission, redirect to `/index` and new book is visible
|
||||
- [ ] ERT tests still pass (model layer unchanged)
|
||||
Reference in New Issue
Block a user