Files
spine/docs/specs/2026-06-21-add-book-form-design.md

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:

  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

(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)