;; 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")))