Files
rose-ash/events/sexp/admin.sexpr
giles 22802bd36b
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
Send all responses as sexp wire format with client-side rendering
- 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>
2026-03-01 09:45:07 +00:00

97 lines
4.7 KiB
Plaintext

;; Events admin components
(defcomp ~events-calendar-admin-panel (&key description-content csrf description)
(section :class "max-w-3xl mx-auto p-4 space-y-10"
(div
(h2 :class "text-xl font-semibold" "Calendar configuration")
(div :id "cal-put-errors" :class "mt-2 text-sm text-red-600")
(div (label :class "block text-sm font-medium text-stone-700" "Description")
(when description-content description-content))
(form :id "calendar-form" :method "post" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-on:beforeRequest "document.querySelector('#cal-put-errors').textContent='';"
:sx-on:responseError "document.querySelector('#cal-put-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
:class "hidden space-y-4 mt-4" :autocomplete "off"
(input :type "hidden" :name "csrf_token" :value csrf)
(div (label :class "block text-sm font-medium text-stone-700" "Description")
(div description)
(textarea :name "description" :autocomplete "off" :rows "4" :class "w-full p-2 border rounded" description))
(div (button :class "px-3 py-2 rounded bg-stone-800 text-white" "Save"))))
(hr :class "border-stone-200")))
(defcomp ~events-entry-admin-link (&key href)
(a :href href :class "inline-flex items-center gap-1 px-2 py-1 text-xs text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded"
(i :class "fa fa-cog" :aria-hidden "true") " Admin"))
(defcomp ~events-entry-field (&key label content)
(div :class "flex flex-col mb-4"
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label)
content))
(defcomp ~events-entry-name-field (&key name)
(div :class "mt-1 text-lg font-medium" name))
(defcomp ~events-entry-slot-assigned (&key slot-name flex-label)
(div :class "mt-1"
(span :class "px-2 py-1 rounded text-sm bg-blue-100 text-blue-700" slot-name)
(span :class "ml-2 text-xs text-stone-500" flex-label)))
(defcomp ~events-entry-slot-none ()
(div :class "mt-1" (span :class "text-sm text-stone-400" "No slot assigned")))
(defcomp ~events-entry-time-field (&key time-str)
(div :class "mt-1" time-str))
(defcomp ~events-entry-state-field (&key entry-id badge)
(div :class "mt-1" (div :id (str "entry-state-" entry-id) badge)))
(defcomp ~events-entry-cost-field (&key cost)
(div :class "mt-1" (span :class "font-medium text-green-600" cost)))
(defcomp ~events-entry-tickets-field (&key entry-id tickets-config)
(div :class "mt-1" :id (str "entry-tickets-" entry-id) tickets-config))
(defcomp ~events-entry-date-field (&key date-str)
(div :class "mt-1" date-str))
(defcomp ~events-entry-posts-field (&key entry-id posts-panel)
(div :class "mt-1" :id (str "entry-posts-" entry-id) posts-panel))
(defcomp ~events-entry-panel (&key entry-id list-container name slot time state cost
tickets buy date posts options pre-action edit-url)
(section :id (str "entry-" entry-id) :class list-container
name slot time state cost
tickets buy date posts
(div :class "flex gap-2 mt-6"
options
(button :type "button" :class pre-action
:sx-get edit-url :sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
"Edit"))))
(defcomp ~events-entry-title (&key name badge)
(<> (i :class "fa fa-clock") " " name " " badge))
(defcomp ~events-entry-times (&key time-str)
(div :class "text-sm text-gray-600" time-str))
(defcomp ~events-entry-optioned-oob (&key entry-id title state)
(<> (div :id (str "entry-title-" entry-id) :sx-swap-oob "innerHTML" title)
(div :id (str "entry-state-" entry-id) :sx-swap-oob "innerHTML" state)))
(defcomp ~events-entry-options (&key entry-id buttons)
(div :id (str "calendar_entry_options_" entry-id) :class "flex flex-col md:flex-row gap-1"
buttons))
(defcomp ~events-entry-option-button (&key url target csrf btn-type action-btn confirm-title confirm-text
label is-btn)
(form :sx-post url :sx-select target :sx-target target :sx-swap "outerHTML"
:sx-trigger (if is-btn "confirmed" nil)
(input :type "hidden" :name "csrf_token" :value csrf)
(button :type btn-type :class action-btn
:data-confirm "true" :data-confirm-title confirm-title
:data-confirm-text confirm-text :data-confirm-icon "question"
:data-confirm-confirm-text (str "Yes, " label " it")
:data-confirm-cancel-text "Cancel"
:data-confirm-event (if is-btn "confirmed" nil)
(i :class "fa-solid fa-rotate mr-2" :aria-hidden "true") label)))