Send all responses as sexp wire format with client-side rendering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
;; Events ticket components
|
||||
|
||||
(defcomp ~events-ticket-card (&key href entry-name type-name time-str cal-name badge-html code-prefix)
|
||||
(defcomp ~events-ticket-card (&key href entry-name type-name time-str cal-name badge code-prefix)
|
||||
(a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "flex-1 min-w-0"
|
||||
@@ -9,20 +9,20 @@
|
||||
(when time-str (div :class "text-sm text-stone-500 mt-1" time-str))
|
||||
(when cal-name (div :class "text-xs text-stone-400 mt-0.5" cal-name)))
|
||||
(div :class "flex flex-col items-end gap-1 flex-shrink-0"
|
||||
(raw! badge-html)
|
||||
badge
|
||||
(span :class "text-xs text-stone-400 font-mono" (str code-prefix "..."))))))
|
||||
|
||||
(defcomp ~events-tickets-panel (&key list-container has-tickets cards-html)
|
||||
(defcomp ~events-tickets-panel (&key list-container has-tickets cards)
|
||||
(section :id "tickets-list" :class list-container
|
||||
(h1 :class "text-2xl font-bold mb-6" "My Tickets")
|
||||
(if has-tickets
|
||||
(div :class "space-y-4" (raw! cards-html))
|
||||
(div :class "space-y-4" cards)
|
||||
(div :class "text-center py-12 text-stone-500"
|
||||
(i :class "fa fa-ticket text-4xl mb-4 block" :aria-hidden "true")
|
||||
(p :class "text-lg" "No tickets yet")
|
||||
(p :class "text-sm mt-1" "Tickets will appear here after you purchase them.")))))
|
||||
|
||||
(defcomp ~events-ticket-detail (&key list-container back-href header-bg entry-name badge-html
|
||||
(defcomp ~events-ticket-detail (&key list-container back-href header-bg entry-name badge
|
||||
type-name code time-date time-range cal-name
|
||||
type-desc checkin-str qr-script)
|
||||
(section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto")
|
||||
@@ -32,7 +32,7 @@
|
||||
(div :class (str "px-6 py-4 border-b border-stone-100 " header-bg)
|
||||
(div :class "flex items-center justify-between"
|
||||
(h1 :class "text-xl font-bold" entry-name)
|
||||
(raw! badge-html))
|
||||
badge)
|
||||
(when type-name (div :class "text-sm text-stone-600 mt-1" type-name)))
|
||||
(div :class "px-6 py-8 flex flex-col items-center border-b border-stone-100"
|
||||
(div :id (str "ticket-qr-" code) :class "bg-white p-4 rounded-lg border border-stone-200")
|
||||
@@ -63,7 +63,7 @@
|
||||
(div :class "text-xs text-stone-500" date-str))
|
||||
|
||||
(defcomp ~events-ticket-admin-checkin-form (&key checkin-url code csrf)
|
||||
(form :hx-post checkin-url :hx-target (str "#ticket-row-" code) :hx-swap "outerHTML"
|
||||
(form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition"
|
||||
(i :class "fa fa-check mr-1" :aria-hidden "true") "Check in")))
|
||||
@@ -72,18 +72,18 @@
|
||||
(span :class "text-xs text-blue-600"
|
||||
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))
|
||||
|
||||
(defcomp ~events-ticket-admin-row (&key code code-short entry-name date-html type-name badge-html action-html)
|
||||
(defcomp ~events-ticket-admin-row (&key code code-short entry-name date type-name badge action)
|
||||
(tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code)
|
||||
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) (raw! date-html))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
|
||||
(td :class "px-4 py-3 text-sm" type-name)
|
||||
(td :class "px-4 py-3" (raw! badge-html))
|
||||
(td :class "px-4 py-3" (raw! action-html))))
|
||||
(td :class "px-4 py-3" badge)
|
||||
(td :class "px-4 py-3" action)))
|
||||
|
||||
(defcomp ~events-ticket-admin-panel (&key list-container stats-html lookup-url has-tickets rows-html)
|
||||
(defcomp ~events-ticket-admin-panel (&key list-container stats lookup-url has-tickets rows)
|
||||
(section :id "ticket-admin" :class list-container
|
||||
(h1 :class "text-2xl font-bold mb-6" "Ticket Admin")
|
||||
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" (raw! stats-html))
|
||||
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats)
|
||||
(div :class "rounded-xl border border-stone-200 bg-white p-6 mb-8"
|
||||
(h2 :class "text-lg font-semibold mb-4"
|
||||
(i :class "fa fa-qrcode mr-2" :aria-hidden "true") "Scan / Look Up Ticket")
|
||||
@@ -91,8 +91,8 @@
|
||||
(input :type "text" :id "ticket-code-input" :name "code"
|
||||
:placeholder "Enter or scan ticket code..."
|
||||
:class "flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
:hx-get lookup-url :hx-trigger "keyup changed delay:300ms"
|
||||
:hx-target "#lookup-result" :hx-include "this" :autofocus "true")
|
||||
:sx-get lookup-url :sx-trigger "keyup changed delay:300ms"
|
||||
:sx-target "#lookup-result" :sx-include "this" :autofocus "true")
|
||||
(button :type "button"
|
||||
:class "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||
:onclick "document.getElementById('ticket-code-input').dispatchEvent(new Event('keyup'))"
|
||||
@@ -110,19 +110,19 @@
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "Type")
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "State")
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "Actions")))
|
||||
(tbody :class "divide-y divide-stone-100" (raw! rows-html))))
|
||||
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet")))))
|
||||
(tbody :class "divide-y divide-stone-100" rows))
|
||||
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet"))))))
|
||||
|
||||
(defcomp ~events-checkin-error (&key message)
|
||||
(div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"
|
||||
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
|
||||
|
||||
(defcomp ~events-checkin-success-row (&key code code-short entry-name date-html type-name badge-html time-str)
|
||||
(defcomp ~events-checkin-success-row (&key code code-short entry-name date type-name badge time-str)
|
||||
(tr :class "bg-blue-50" :id (str "ticket-row-" code)
|
||||
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) (raw! date-html))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
|
||||
(td :class "px-4 py-3 text-sm" type-name)
|
||||
(td :class "px-4 py-3" (raw! badge-html))
|
||||
(td :class "px-4 py-3" badge)
|
||||
(td :class "px-4 py-3"
|
||||
(span :class "text-xs text-blue-600"
|
||||
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))))
|
||||
@@ -143,14 +143,14 @@
|
||||
(defcomp ~events-lookup-cal (&key cal-name)
|
||||
(div :class "text-xs text-stone-400 mt-0.5" cal-name))
|
||||
|
||||
(defcomp ~events-lookup-status (&key badge-html code)
|
||||
(div :class "mt-2" (raw! badge-html) (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
|
||||
(defcomp ~events-lookup-status (&key badge code)
|
||||
(div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
|
||||
|
||||
(defcomp ~events-lookup-checkin-time (&key date-str)
|
||||
(div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str)))
|
||||
|
||||
(defcomp ~events-lookup-checkin-btn (&key checkin-url code csrf)
|
||||
(form :hx-post checkin-url :hx-target (str "#checkin-action-" code) :hx-swap "innerHTML"
|
||||
(form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit"
|
||||
:class "px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold text-lg"
|
||||
@@ -166,26 +166,26 @@
|
||||
(i :class "fa fa-times-circle text-3xl" :aria-hidden "true")
|
||||
(div :class "text-sm font-medium mt-1" "Cancelled")))
|
||||
|
||||
(defcomp ~events-lookup-card (&key info-html code action-html)
|
||||
(defcomp ~events-lookup-card (&key info code action)
|
||||
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-4"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "flex-1" (raw! info-html))
|
||||
(div :id (str "checkin-action-" code) (raw! action-html)))))
|
||||
(div :class "flex-1" info)
|
||||
(div :id (str "checkin-action-" code) action))))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-row (&key code code-short type-name badge-html action-html)
|
||||
(defcomp ~events-entry-tickets-admin-row (&key code code-short type-name badge action)
|
||||
(tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code)
|
||||
(td :class "px-4 py-2 font-mono text-xs" code-short)
|
||||
(td :class "px-4 py-2" type-name)
|
||||
(td :class "px-4 py-2" (raw! badge-html))
|
||||
(td :class "px-4 py-2" (raw! action-html))))
|
||||
(td :class "px-4 py-2" badge)
|
||||
(td :class "px-4 py-2" action)))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-checkin (&key checkin-url code csrf)
|
||||
(form :hx-post checkin-url :hx-target (str "#entry-ticket-row-" code) :hx-swap "outerHTML"
|
||||
(form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
|
||||
"Check in")))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-table (&key rows-html)
|
||||
(defcomp ~events-entry-tickets-admin-table (&key rows)
|
||||
(div :class "overflow-x-auto rounded-xl border border-stone-200"
|
||||
(table :class "w-full text-sm"
|
||||
(thead :class "bg-stone-50"
|
||||
@@ -193,14 +193,14 @@
|
||||
(th :class "px-4 py-2 text-left font-medium text-stone-600" "Type")
|
||||
(th :class "px-4 py-2 text-left font-medium text-stone-600" "State")
|
||||
(th :class "px-4 py-2 text-left font-medium text-stone-600" "Actions")))
|
||||
(tbody :class "divide-y divide-stone-100" (raw! rows-html)))))
|
||||
(tbody :class "divide-y divide-stone-100" rows))))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-empty ()
|
||||
(div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry"))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-panel (&key entry-name count-label body-html)
|
||||
(defcomp ~events-entry-tickets-admin-panel (&key entry-name count-label body)
|
||||
(div :class "space-y-4"
|
||||
(div :class "flex items-center justify-between"
|
||||
(h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name))
|
||||
(span :class "text-sm text-stone-500" count-label))
|
||||
(raw! body-html)))
|
||||
body))
|
||||
|
||||
Reference in New Issue
Block a user