Files
mono/events/sx/forms.sx
giles e8bc228c7f Rebrand sexp → sx across web platform (173 files)
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:06:57 +00:00

502 lines
23 KiB
Plaintext

;; Events form components — entry edit, slot add/edit, entry add,
;; ticket type add/edit, add buttons, post search results.
;; ---------------------------------------------------------------------------
;; Slot picker option (shared by entry-edit and entry-add)
;; ---------------------------------------------------------------------------
(defcomp ~events-slot-option (&key value data-start data-end data-flexible data-cost selected label)
(option :value value :data-start data-start :data-end data-end
:data-flexible data-flexible :data-cost data-cost
:selected selected
label))
(defcomp ~events-slot-picker (&key id options)
(select :id id :name "slot_id" :class "w-full border p-2 rounded"
:data-slot-picker "" :required "required"
options))
(defcomp ~events-no-slots ()
(div :class "text-sm text-stone-500" "No slots defined for this day."))
;; ---------------------------------------------------------------------------
;; Entry edit form (_types/entry/_edit.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-entry-edit-form (&key entry-id list-container put-url cancel-url csrf
name-val slot-picker
start-val end-val cost-display
ticket-price-val ticket-count-val
action-btn cancel-btn)
(section :id (str "entry-" entry-id) :class list-container
(div :id (str "entry-errors-" entry-id) :class "mt-2 text-sm text-red-600")
(form :class "space-y-3 mt-4" :sx-put put-url
:sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf)
;; Name
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "entry-name-" entry-id) "Name")
(input :id (str "entry-name-" entry-id) :name "name"
:class "w-full border p-2 rounded" :placeholder "Name"
:value name-val))
;; Slot picker
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "entry-slot-" entry-id) "Slot")
slot-picker)
;; Time inputs (flexible slots)
(div :data-time-fields "" :class "hidden space-y-3"
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "entry-start-" entry-id) "From")
(input :id (str "entry-start-" entry-id) :name "start_at" :type "time"
:class "w-full border p-2 rounded" :value start-val
:data-entry-start ""))
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "entry-end-" entry-id) "To")
(input :id (str "entry-end-" entry-id) :name "end_at" :type "time"
:class "w-full border p-2 rounded" :value end-val
:data-entry-end ""))
(p :class "text-xs text-stone-500" :data-slot-boundary ""))
;; Fixed time summary
(div :data-fixed-summary "" :class "hidden text-sm text-stone-600")
;; Cost display
(div :data-cost-row "" :class "hidden text-sm font-medium text-stone-700"
"Estimated Cost: "
(span :data-cost-display "" :class "text-green-600" cost-display))
;; Ticket Configuration
(div :class "border-t pt-3 mt-3"
(h4 :class "text-sm font-semibold text-stone-700 mb-3" "Ticket Configuration")
(div :class "grid grid-cols-2 gap-3"
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "entry-ticket-price-" entry-id)
"Ticket Price (£)")
(input :id (str "entry-ticket-price-" entry-id) :name "ticket_price"
:type "number" :step "0.01" :min "0"
:class "w-full border p-2 rounded"
:placeholder "Leave empty for no tickets"
:value ticket-price-val)
(p :class "text-xs text-stone-500 mt-1" "Leave empty if no tickets needed"))
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "entry-ticket-count-" entry-id) "Total Tickets")
(input :id (str "entry-ticket-count-" entry-id) :name "ticket_count"
:type "number" :min "0"
:class "w-full border p-2 rounded"
:placeholder "Leave empty for unlimited"
:value ticket-count-val)
(p :class "text-xs text-stone-500 mt-1" "Leave empty for unlimited"))))
;; Buttons
(div :class "flex justify-end gap-2 pt-2"
(button :type "button" :class cancel-btn
:sx-get cancel-url
:sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
"Cancel")
(button :type "submit" :class action-btn
:data-confirm "true" :data-confirm-title "Save entry?"
:data-confirm-text "Are you sure you want to save this entry?"
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, save it"
:data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save entry")))))
;; ---------------------------------------------------------------------------
;; Post search results (_types/entry/_post_search_results.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-post-search-item (&key post-url entry-id csrf post-id
img title)
(form :sx-post post-url :sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML"
:class "p-2 hover:bg-stone-50 cursor-pointer rounded text-sm border-b"
(input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "post_id" :value post-id)
(button :type "submit" :class "w-full text-left flex items-center gap-2"
:data-confirm "" :data-confirm-title "Add post?"
:data-confirm-text (str "Add " title " to this entry?")
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, add it"
:data-confirm-cancel-text "Cancel"
img (span title))))
(defcomp ~events-post-search-sentinel (&key page next-url)
(div :id (str "post-search-sentinel-" page)
:sx-get next-url
:sx-trigger "intersect once delay:250ms, sentinel:retry"
:sx-swap "outerHTML"
:_ "
init
if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end
on sentinel:retry
remove .hidden from .js-loading in me
add .hidden to .js-neterr in me
set me.style.pointerEvents to 'none'
set me.style.opacity to '0'
trigger htmx:consume on me
call htmx.trigger(me, 'intersect')
end
def backoff()
add .hidden to .js-loading in me
remove .hidden from .js-neterr in me
set myMs to Number(me.dataset.retryMs)
if myMs < 10000 then set me.dataset.retryMs to myMs * 2 end
js setTimeout(() => htmx.trigger(me, 'sentinel:retry'), myMs)
end
on htmx:beforeRequest
set me.style.pointerEvents to 'none'
set me.style.opacity to '0'
end
on htmx:afterSwap
set me.dataset.retryMs to 1000
end
on htmx:sendError call backoff()
on htmx:responseError call backoff()
on htmx:timeout call backoff()
"
:role "status" :aria-live "polite" :aria-hidden "true" :class "py-2"
(div :class "text-xs text-center text-stone-400 js-loading" "Loading more...")
(div :class "text-xs text-center text-stone-400 js-neterr hidden" "Connection error. Retrying...")))
(defcomp ~events-post-search-end ()
(div :class "py-2 text-xs text-center text-stone-400" "End of results"))
;; ---------------------------------------------------------------------------
;; Slot edit form (_types/slot/_edit.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-day-checkbox (&key name label checked)
(label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-100"
(input :type "checkbox" :name name :value "1" :data-day name :checked checked)
(span label)))
(defcomp ~events-day-all-checkbox (&key checked)
(label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-200"
(input :type "checkbox" :data-day-all "" :checked checked)
(span "All")))
(defcomp ~events-slot-edit-form (&key slot-id list-container put-url cancel-url csrf
name-val cost-val start-val end-val desc-val
days flexible-checked
action-btn cancel-btn)
(section :id (str "slot-" slot-id) :class list-container
(div :id "slot-errors" :class "mt-2 text-sm text-red-600")
(form :class "space-y-3 mt-4" :sx-put put-url
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML"
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
(input :type "hidden" :name "csrf_token" :value csrf)
;; Name
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "slot-name-" slot-id) "Name")
(input :id (str "slot-name-" slot-id) :name "name" :placeholder "Name"
:class "w-full border p-2 rounded" :value name-val))
;; Cost
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "slot-cost-" slot-id) "Cost")
(input :id (str "slot-cost-" slot-id) :name "cost" :placeholder "Cost e.g. 12.50"
:class "w-full border p-2 rounded" :value cost-val))
;; Start time
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "slot-start-" slot-id) "Start time")
(input :id (str "slot-start-" slot-id) :name "time_start" :placeholder "Start HH:MM"
:class "w-full border p-2 rounded" :value start-val))
;; End time
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "slot-end-" slot-id) "End time")
(input :id (str "slot-end-" slot-id) :name "time_end" :placeholder "End HH:MM"
:class "w-full border p-2 rounded" :value end-val))
;; Description
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "slot-desc-" slot-id) "Description")
(textarea :id (str "slot-desc-" slot-id) :name "description" :rows "2"
:placeholder "Description" :class "w-full border p-2 rounded"
desc-val))
;; Days
(div
(span :class "block text-sm font-medium text-stone-700 mb-1" "Days")
(div :class "flex flex-wrap gap-3 items-center text-sm" :data-days-group ""
days))
;; Flexible
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "slot-flexible-" slot-id) "Flexible booking")
(label :class "inline-flex items-center gap-2 text-xs"
(input :id (str "slot-flexible-" slot-id) :type "checkbox" :name "flexible"
:value "1" :checked flexible-checked)
(span "Allow bookings at any time within this band")))
;; Buttons
(div :class "flex justify-end gap-2 pt-2"
(button :type "button" :class cancel-btn
:sx-get cancel-url
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML"
"Cancel")
(button :type "submit" :class action-btn
:data-confirm "true" :data-confirm-title "Save slot?"
:data-confirm-text "Are you sure you want to save this slot?"
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, save it"
:data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save slot")))))
;; ---------------------------------------------------------------------------
;; Slot add form (_types/slots/_add.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-slot-add-form (&key post-url csrf days action-btn cancel-btn cancel-url)
(form :sx-post post-url :sx-target "#slots-table" :sx-select "#slots-table"
:sx-disinherit "sx-select" :sx-swap "outerHTML"
:sx-headers csrf :class "space-y-3"
(div :class "grid grid-cols-1 md:grid-cols-4 gap-3"
(div :class "md:col-span-2"
(label :class "block text-xs font-semibold mb-1" "Name")
(input :type "text" :name "name" :class "w-full border rounded px-2 py-1 text-sm" :required "required"))
(div :class "md:col-span-2"
(label :class "block text-xs font-semibold mb-1" "Description")
(input :type "text" :name "description" :class "w-full border rounded px-2 py-1 text-sm"))
(div
(label :class "block text-xs font-semibold mb-1" "Days")
(div :class "flex flex-wrap gap-1 text-xs" :data-days-group ""
days))
(div
(label :class "block text-xs font-semibold mb-1" "Time start")
(input :type "time" :name "time_start" :class "w-full border rounded px-2 py-1 text-sm" :required "required"))
(div
(label :class "block text-xs font-semibold mb-1" "Time end")
(input :type "time" :name "time_end" :class "w-full border rounded px-2 py-1 text-sm" :required "required"))
(div
(label :class "block text-xs font-semibold mb-1" "Cost")
(input :type "text" :name "cost" :class "w-full border rounded px-2 py-1 text-sm" :placeholder "e.g. 5.00"))
(div :class "md:col-span-2"
(label :class "block text-xs font-semibold mb-1" "Flexible booking")
(label :class "inline-flex items-center gap-2 text-xs"
(input :type "checkbox" :name "flexible" :value "1")
(span "Allow bookings at any time within this band"))))
(div :class "flex justify-end gap-2 pt-2"
(button :type "button" :class cancel-btn
:sx-get cancel-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
"Cancel")
(button :type "submit" :class action-btn
:data-confirm "true" :data-confirm-title "Add slot?"
:data-confirm-text "Are you sure you want to add this slot?"
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, add it"
:data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save slot"))))
(defcomp ~events-slot-add-button (&key pre-action add-url)
(button :type "button" :class pre-action
:sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
"+ Add slot"))
;; ---------------------------------------------------------------------------
;; Entry add form (_types/day/_add.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-entry-add-form (&key post-url csrf slot-picker
action-btn cancel-btn cancel-url)
(<>
(div :id "entry-errors" :class "mt-2 text-sm text-red-600")
(form :class "mt-4 grid grid-cols-1 md:grid-cols-4 gap-2"
:sx-post post-url :sx-target "#day-entries"
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
:sx-swap "innerHTML"
(input :type "hidden" :name "csrf_token" :value csrf)
;; Entry name
(input :name "name" :type "text" :required "required"
:class "border rounded px-3 py-2" :placeholder "Entry name")
;; Slot picker
slot-picker
;; Time entry + cost display
(div :class "md:col-span-2 flex flex-col gap-2"
;; Time inputs (flexible)
(div :data-time-fields "" :class "hidden"
(div :class "mb-2"
(label :class "block text-xs font-medium text-stone-700 mb-1" "From")
(input :name "start_time" :type "time" :class "border rounded px-3 py-2 w-full"
:data-entry-start ""))
(div :class "mb-2"
(label :class "block text-xs font-medium text-stone-700 mb-1" "To")
(input :name "end_time" :type "time" :class "border rounded px-3 py-2 w-full"
:data-entry-end ""))
(p :class "text-xs text-stone-500" :data-slot-boundary ""))
;; Cost display
(div :data-cost-row "" :class "hidden text-sm font-medium text-stone-700"
"Estimated Cost: "
(span :data-cost-display "" :class "text-green-600" "£0.00"))
;; Fixed summary
(div :data-fixed-summary "" :class "hidden text-sm text-stone-600"))
;; Ticket Configuration
(div :class "md:col-span-4 border-t pt-3 mt-2"
(h4 :class "text-sm font-semibold text-stone-700 mb-3" "Ticket Configuration (Optional)")
(div :class "grid grid-cols-2 gap-3"
(div
(label :class "block text-xs font-medium text-stone-700 mb-1"
"Ticket Price (£)")
(input :name "ticket_price" :type "number" :step "0.01" :min "0"
:class "w-full border rounded px-3 py-2 text-sm"
:placeholder "Leave empty for no tickets"))
(div
(label :class "block text-xs font-medium text-stone-700 mb-1" "Total Tickets")
(input :name "ticket_count" :type "number" :min "0"
:class "w-full border rounded px-3 py-2 text-sm"
:placeholder "Leave empty for unlimited"))))
;; Buttons
(div :class "flex justify-end gap-2 pt-2 md:col-span-4"
(button :type "button" :class cancel-btn
:sx-get cancel-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
"Cancel")
(button :type "submit" :class action-btn
:data-confirm "true" :data-confirm-title "Add entry?"
:data-confirm-text "Are you sure you want to add this entry?"
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, add it"
:data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save entry")))))
(defcomp ~events-entry-add-button (&key pre-action add-url)
(button :type "button" :class pre-action
:sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
"+ Add entry"))
;; ---------------------------------------------------------------------------
;; Ticket type edit form (_types/ticket_type/_edit.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-type-edit-form (&key ticket-id list-container put-url cancel-url csrf
name-val cost-val count-val
action-btn cancel-btn)
(section :id (str "ticket-" ticket-id) :class list-container
(div :id "ticket-errors" :class "mt-2 text-sm text-red-600")
(form :class "space-y-3 mt-4" :sx-put put-url
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML"
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
(input :type "hidden" :name "csrf_token" :value csrf)
;; Name
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "ticket-name-" ticket-id) "Name")
(input :id (str "ticket-name-" ticket-id) :name "name"
:placeholder "e.g. Adult, Child, Student"
:class "w-full border p-2 rounded" :value name-val :required "required"))
;; Cost
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "ticket-cost-" ticket-id) "Cost (£)")
(input :id (str "ticket-cost-" ticket-id) :name "cost" :type "number"
:step "0.01" :min "0" :placeholder "e.g. 5.00"
:class "w-full border p-2 rounded" :value cost-val :required "required"))
;; Count
(div
(label :class "block text-sm font-medium text-stone-700 mb-1"
:for (str "ticket-count-" ticket-id) "Count")
(input :id (str "ticket-count-" ticket-id) :name "count" :type "number"
:min "0" :placeholder "e.g. 50"
:class "w-full border p-2 rounded" :value count-val :required "required"))
;; Buttons
(div :class "flex justify-end gap-2 pt-2"
(button :type "button" :class cancel-btn
:sx-get cancel-url
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML"
"Cancel")
(button :type "submit" :class action-btn
:data-confirm "true" :data-confirm-title "Save ticket type?"
:data-confirm-text "Are you sure you want to save this ticket type?"
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, save it"
:data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save ticket type")))))
;; ---------------------------------------------------------------------------
;; Ticket type add form (_types/ticket_types/_add.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-type-add-form (&key post-url csrf action-btn cancel-btn cancel-url)
(form :sx-post post-url :sx-target "#tickets-table" :sx-select "#tickets-table"
:sx-disinherit "sx-select" :sx-swap "outerHTML"
:sx-headers csrf :class "space-y-3"
(div :class "grid grid-cols-1 md:grid-cols-3 gap-3"
(div
(label :class "block text-xs font-semibold mb-1" "Name")
(input :type "text" :name "name" :class "w-full border rounded px-2 py-1 text-sm"
:placeholder "e.g. Adult, Child, Student" :required "required"))
(div
(label :class "block text-xs font-semibold mb-1" "Cost (£)")
(input :type "number" :name "cost" :step "0.01" :min "0"
:class "w-full border rounded px-2 py-1 text-sm"
:placeholder "e.g. 5.00" :required "required"))
(div
(label :class "block text-xs font-semibold mb-1" "Count")
(input :type "number" :name "count" :min "0"
:class "w-full border rounded px-2 py-1 text-sm"
:placeholder "e.g. 50" :required "required")))
(div :class "flex justify-end gap-2 pt-2"
(button :type "button" :class cancel-btn
:sx-get cancel-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
"Cancel")
(button :type "submit" :class action-btn
:data-confirm "true" :data-confirm-title "Add ticket type?"
:data-confirm-text "Are you sure you want to add this ticket type?"
:data-confirm-icon "question"
:data-confirm-confirm-text "Yes, add it"
:data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save ticket type"))))
(defcomp ~events-ticket-type-add-button (&key action-btn add-url)
(button :class action-btn
:sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
(i :class "fa fa-plus") " Add ticket type"))
;; ---------------------------------------------------------------------------
;; Entry admin nav — placeholder
;; ---------------------------------------------------------------------------
(defcomp ~events-admin-placeholder-nav ()
(div :class "relative nav-group"
(span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options")))
;; Entry admin main panel — ticket_types link
(defcomp ~events-entry-admin-main-panel (&key link)
link)