feat: add TodoMVC example with editorial design

Full TodoMVC implementation using Pocketbook's synced atom:
- Add, toggle, edit (double-click), destroy todos
- Filter: All / Active / Completed
- Toggle all, clear completed
- Live sync status indicator (online/offline/pending)
- Data persists in IndexedDB, syncs to server via Transit

Design: Ink & Paper aesthetic — cream paper background with grain
texture, Instrument Serif italic header, Newsreader body text,
DM Mono UI chrome, terracotta accent with green completion marks,
deckled left border on card.

Also:
- Server now serves static files from resources/public (/ → todomvc.html)
- Fix CLJS compilation: resolve close!/put! naming conflicts with
  core.async, use qualified async/close!, add clojure.string require
- Fix unbalanced parens in do-pull!
- Remove old placeholder example
This commit is contained in:
Florian Schroedl
2026-04-04 16:45:14 +02:00
parent 55cddf751b
commit ab68a21dd6
10 changed files with 770 additions and 156 deletions

View File

@@ -11,7 +11,8 @@
(:require [org.httpkit.server :as http]
[pocketbook.db :as db]
[pocketbook.transit :as t]
[clojure.string :as str])
[clojure.string :as str]
[clojure.java.io :as io])
(:gen-class))
;; ---------------------------------------------------------------------------
@@ -111,6 +112,43 @@
:when k]
[k (or v "")]))))
;; ---------------------------------------------------------------------------
;; Static file serving
;; ---------------------------------------------------------------------------
(def ^:private content-types
{"html" "text/html; charset=utf-8"
"css" "text/css; charset=utf-8"
"js" "application/javascript; charset=utf-8"
"json" "application/json"
"svg" "image/svg+xml"
"png" "image/png"
"jpg" "image/jpeg"
"ico" "image/x-icon"
"woff2" "font/woff2"
"woff" "font/woff"
"map" "application/json"})
(defn- ext [path]
(let [i (str/last-index-of path ".")]
(when (and i (pos? i))
(subs path (inc i)))))
(defn- serve-static
"Attempt to serve a static file from resources/public. Returns response or nil."
[uri]
(let [path (str "public" (if (= "/" uri) "/todomvc.html" uri))
resource (io/resource path)]
(when resource
{:status 200
:headers {"Content-Type" (get content-types (ext path) "application/octet-stream")
"Cache-Control" "no-cache"}
:body (io/input-stream resource)})))
;; ---------------------------------------------------------------------------
;; Ring handler
;; ---------------------------------------------------------------------------
(defn make-handler
"Create the Ring handler function."
[ds config]
@@ -121,12 +159,6 @@
(= :options (:request-method req))
{:status 204 :headers {} :body nil}
;; Health check
(= "/" (:uri req))
{:status 200
:headers {"Content-Type" "text/plain"}
:body "pocketbook ok"}
;; Sync endpoints
(= "/sync" (:uri req))
(let [user (authenticate config req)]
@@ -137,10 +169,12 @@
:post (handle-push ds user req)
(transit-response 405 {:error "Method not allowed"}))))
;; Static files (including / → todomvc.html)
:else
{:status 404
:headers {"Content-Type" "text/plain"}
:body "Not found"})]
(or (serve-static (:uri req))
{:status 404
:headers {"Content-Type" "text/plain"}
:body "Not found"}))]
(if (:cors config)
(cors-headers resp)
resp))))
@@ -161,6 +195,7 @@
(println (str "🔶 Pocketbook server running on http://localhost:" (:port config)))
(println (str " Database: " (:db-path config)))
(println (str " Auth: " (if (:users config) "enabled" "disabled")))
(println (str " TodoMVC: http://localhost:" (:port config) "/todomvc.html"))
{:stop server
:ds ds
:config config})))