5.7 KiB
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:
{
"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:
<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:
<a role="button" href="/add">Add book</a>
Write function: spine-add-book
(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:
- Visits
spine-org-filein a temporary buffer - Inserts a new top-level headline with TODO keyword
WANT - Sets properties:
:AUTHOR:,:FORMAT:,:ISBN:,:COVER:,:REC_BY:,:REC_NOTE:,:ADDED:,:ID: - Sets tags if category provided
- Appends initial note as
- [DATE] note textif provided - Saves the buffer
- 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
(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 /addshows form with Pico styling- Author and category fields have datalist suggestions from existing data
- Submitting the form creates a new
* WANT Titleheadline inspine.org - Properties are set correctly (title, author, format, ISBN, etc.)
- Initial note appears as
- [YYYY-MM-DD] textin the body - Empty optional fields are omitted (no empty properties)
- After submission, redirect to
/indexand new book is visible - ERT tests still pass (model layer unchanged)