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>
This commit is contained in:
0
events/sx/__init__.py
Normal file
0
events/sx/__init__.py
Normal file
96
events/sx/admin.sx
Normal file
96
events/sx/admin.sx
Normal file
@@ -0,0 +1,96 @@
|
||||
;; 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)))
|
||||
102
events/sx/calendar.sx
Normal file
102
events/sx/calendar.sx
Normal file
@@ -0,0 +1,102 @@
|
||||
;; Events calendar components
|
||||
|
||||
(defcomp ~events-calendar-nav-arrow (&key pill-cls href label)
|
||||
(a :class (str pill-cls " text-xl") :href href
|
||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label))
|
||||
|
||||
(defcomp ~events-calendar-month-label (&key month-name year)
|
||||
(div :class "px-3 font-medium" (str month-name " " year)))
|
||||
|
||||
(defcomp ~events-calendar-weekday (&key name)
|
||||
(div :class "py-1" name))
|
||||
|
||||
(defcomp ~events-calendar-day-short (&key day-str)
|
||||
(span :class "sm:hidden text-[16px] text-stone-500" day-str))
|
||||
|
||||
(defcomp ~events-calendar-day-num (&key pill-cls href num)
|
||||
(a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true" num))
|
||||
|
||||
(defcomp ~events-calendar-entry-badge (&key bg-cls name state-label)
|
||||
(div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls)
|
||||
(span :class "truncate" name)
|
||||
(span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label)))
|
||||
|
||||
(defcomp ~events-calendar-cell (&key cell-cls day-short day-num badges)
|
||||
(div :class cell-cls
|
||||
(div :class "flex justify-between items-center"
|
||||
(div :class "flex flex-col" day-short day-num))
|
||||
(div :class "mt-1 space-y-0.5" badges)))
|
||||
|
||||
(defcomp ~events-calendar-grid (&key arrows weekdays cells)
|
||||
(section :class "bg-orange-100"
|
||||
(header :class "flex items-center justify-center mt-2"
|
||||
(nav :class "flex items-center gap-2 text-2xl" arrows))
|
||||
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4"
|
||||
(div :class "hidden sm:grid grid-cols-7 text-center text-md font-semibold text-stone-700 mb-2" weekdays)
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells))))
|
||||
|
||||
(defcomp ~events-calendars-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "cal-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#calendars-list" :sx-select "#calendars-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#cal-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#cal-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
|
||||
:placeholder "e.g. Events, Gigs, Meetings"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add calendar"))))
|
||||
|
||||
(defcomp ~events-calendars-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
form
|
||||
(div :id "calendars-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~events-calendars-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No calendars yet. Create one above."))
|
||||
|
||||
(defcomp ~events-calendars-item (&key href cal-name cal-slug del-url csrf-hdr)
|
||||
(div :class "mt-6 border rounded-lg p-4"
|
||||
(div :class "flex items-center justify-between gap-3"
|
||||
(a :class "flex items-baseline gap-3" :href href
|
||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true"
|
||||
(h3 :class "font-semibold" cal-name)
|
||||
(h4 :class "text-gray-500" (str "/" cal-slug "/")))
|
||||
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
|
||||
:data-confirm true :data-confirm-title "Delete calendar?"
|
||||
:data-confirm-text "Entries will be hidden (soft delete)"
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#calendars-list" :sx-select "#calendars-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
(defcomp ~events-calendar-description-display (&key description edit-url)
|
||||
(div :id "calendar-description"
|
||||
(if description
|
||||
(p :class "text-stone-700 whitespace-pre-line break-all" description)
|
||||
(p :class "text-stone-400 italic" "No description yet."))
|
||||
(button :type "button" :class "mt-2 text-xs underline"
|
||||
:sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
(i :class "fas fa-edit"))))
|
||||
|
||||
(defcomp ~events-calendar-description-title-oob (&key description)
|
||||
(div :id "calendar-description-title" :sx-swap-oob "outerHTML"
|
||||
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
description))
|
||||
|
||||
(defcomp ~events-calendar-description-edit-form (&key save-url cancel-url csrf description)
|
||||
(div :id "calendar-description"
|
||||
(form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(textarea :name "description" :autocomplete "off" :rows "4"
|
||||
:class "w-full p-2 border rounded" description)
|
||||
(div :class "mt-2 flex gap-2 text-xs"
|
||||
(button :type "submit" :class "px-3 py-1 rounded bg-stone-800 text-white" "Save")
|
||||
(button :type "button" :class "px-3 py-1 rounded border"
|
||||
:sx-get cancel-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
"Cancel")))))
|
||||
84
events/sx/day.sx
Normal file
84
events/sx/day.sx
Normal file
@@ -0,0 +1,84 @@
|
||||
;; Events day components
|
||||
|
||||
(defcomp ~events-day-entry-link (&key href name time-str)
|
||||
(a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" time-str))))
|
||||
|
||||
(defcomp ~events-day-entries-nav (&key inner)
|
||||
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
|
||||
:id "day-entries-nav-wrapper"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin"
|
||||
inner)))
|
||||
|
||||
(defcomp ~events-day-table (&key list-container rows pre-action add-url)
|
||||
(section :id "day-entries" :class list-container
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
(tr
|
||||
(th :class "p-2 text-left w-2/6" "Name")
|
||||
(th :class "text-left p-2 w-1/6" "Slot/Time")
|
||||
(th :class "text-left p-2 w-1/6" "State")
|
||||
(th :class "text-left p-2 w-1/6" "Cost")
|
||||
(th :class "text-left p-2 w-1/6" "Tickets")
|
||||
(th :class "text-left p-2 w-1/6" "Actions")))
|
||||
(tbody rows))
|
||||
(div :id "entry-add-container" :class "mt-4"
|
||||
(button :type "button" :class pre-action
|
||||
:sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
|
||||
"+ Add entry"))))
|
||||
|
||||
(defcomp ~events-day-empty-row ()
|
||||
(tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet.")))
|
||||
|
||||
(defcomp ~events-day-row-name (&key href pill-cls name)
|
||||
(td :class "p-2 align-top w-2/6" (div :class "font-medium"
|
||||
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true" name))))
|
||||
|
||||
(defcomp ~events-day-row-slot (&key href pill-cls slot-name time-str)
|
||||
(td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium"
|
||||
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true" slot-name)
|
||||
(span :class "text-stone-600 font-normal" time-str))))
|
||||
|
||||
(defcomp ~events-day-row-time (&key start end)
|
||||
(td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end))))
|
||||
|
||||
(defcomp ~events-day-row-state (&key state-id badge)
|
||||
(td :class "p-2 align-top w-1/6" (div :id state-id badge)))
|
||||
|
||||
(defcomp ~events-day-row-cost (&key cost-str)
|
||||
(td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str)))
|
||||
|
||||
(defcomp ~events-day-row-tickets (&key price-str count-str)
|
||||
(td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1"
|
||||
(div :class "font-medium text-green-600" price-str)
|
||||
(div :class "text-stone-600" count-str))))
|
||||
|
||||
(defcomp ~events-day-row-no-tickets ()
|
||||
(td :class "p-2 align-top w-1/6" (span :class "text-xs text-stone-400" "No tickets")))
|
||||
|
||||
(defcomp ~events-day-row-actions ()
|
||||
(td :class "p-2 align-top w-1/6"))
|
||||
|
||||
(defcomp ~events-day-row (&key tr-cls name slot state cost tickets actions)
|
||||
(tr :class tr-cls name slot state cost tickets actions))
|
||||
|
||||
(defcomp ~events-day-admin-panel ()
|
||||
(div :class "p-4 text-sm text-stone-500" "Admin options"))
|
||||
|
||||
(defcomp ~events-day-entries-nav-oob-empty ()
|
||||
(div :id "day-entries-nav-wrapper" :sx-swap-oob "true"))
|
||||
|
||||
(defcomp ~events-day-entries-nav-oob (&key items)
|
||||
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
|
||||
:id "day-entries-nav-wrapper" :sx-swap-oob "true"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
|
||||
|
||||
(defcomp ~events-day-nav-entry (&key href nav-btn name time-str)
|
||||
(a :href href :class nav-btn
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" time-str))))
|
||||
103
events/sx/entries.sx
Normal file
103
events/sx/entries.sx
Normal file
@@ -0,0 +1,103 @@
|
||||
;; Events entry card components (all events / page summary)
|
||||
|
||||
(defcomp ~events-state-badge (&key cls label)
|
||||
(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " cls) label))
|
||||
|
||||
(defcomp ~events-entry-title-linked (&key href name)
|
||||
(a :href href :class "hover:text-emerald-700"
|
||||
(h2 :class "text-lg font-semibold text-stone-900" name)))
|
||||
|
||||
(defcomp ~events-entry-title-plain (&key name)
|
||||
(h2 :class "text-lg font-semibold text-stone-900" name))
|
||||
|
||||
(defcomp ~events-entry-title-tile-linked (&key href name)
|
||||
(a :href href :class "hover:text-emerald-700"
|
||||
(h2 :class "text-base font-semibold text-stone-900 line-clamp-2" name)))
|
||||
|
||||
(defcomp ~events-entry-title-tile-plain (&key name)
|
||||
(h2 :class "text-base font-semibold text-stone-900 line-clamp-2" name))
|
||||
|
||||
(defcomp ~events-entry-page-badge (&key href title)
|
||||
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" title))
|
||||
|
||||
(defcomp ~events-entry-cal-badge (&key name)
|
||||
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700" name))
|
||||
|
||||
(defcomp ~events-entry-time-linked (&key href date-str)
|
||||
(<> (a :href href :class "hover:text-stone-700" date-str) " · "))
|
||||
|
||||
(defcomp ~events-entry-time-plain (&key date-str)
|
||||
(<> (span date-str) " · "))
|
||||
|
||||
(defcomp ~events-entry-cost (&key cost)
|
||||
(div :class "mt-1 text-sm font-medium text-green-600" cost))
|
||||
|
||||
(defcomp ~events-entry-card (&key title badges time-parts cost widget)
|
||||
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-4"
|
||||
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-3"
|
||||
(div :class "flex-1 min-w-0"
|
||||
title
|
||||
(div :class "flex flex-wrap items-center gap-1.5 mt-1" badges)
|
||||
(div :class "mt-1 text-sm text-stone-500" time-parts)
|
||||
cost)
|
||||
widget)))
|
||||
|
||||
(defcomp ~events-entry-card-tile (&key title badges time cost widget)
|
||||
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 overflow-hidden"
|
||||
(div :class "p-3"
|
||||
title
|
||||
(div :class "flex flex-wrap items-center gap-1 mt-1" badges)
|
||||
(div :class "mt-1 text-xs text-stone-500" time)
|
||||
cost)
|
||||
widget))
|
||||
|
||||
(defcomp ~events-entry-tile-widget-wrapper (&key widget)
|
||||
(div :class "border-t border-stone-100 px-3 py-2" widget))
|
||||
|
||||
(defcomp ~events-entry-widget-wrapper (&key widget)
|
||||
(div :class "shrink-0" widget))
|
||||
|
||||
(defcomp ~events-date-separator (&key date-str)
|
||||
(div :class "pt-2 pb-1"
|
||||
(h3 :class "text-sm font-semibold text-stone-500 uppercase tracking-wide" date-str)))
|
||||
|
||||
(defcomp ~events-sentinel (&key page next-url)
|
||||
(div :id (str "sentinel-" page) :class "h-4 opacity-0 pointer-events-none"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
|
||||
:role "status" :aria-hidden "true"
|
||||
(div :class "text-center text-xs text-stone-400" "loading...")))
|
||||
|
||||
(defcomp ~events-list-svg ()
|
||||
(svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none"
|
||||
:viewBox "0 0 24 24" :stroke "currentColor" :stroke-width "2"
|
||||
(path :stroke-linecap "round" :stroke-linejoin "round" :d "M4 6h16M4 12h16M4 18h16")))
|
||||
|
||||
(defcomp ~events-tile-svg ()
|
||||
(svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none"
|
||||
:viewBox "0 0 24 24" :stroke "currentColor" :stroke-width "2"
|
||||
(path :stroke-linecap "round" :stroke-linejoin "round"
|
||||
:d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z")))
|
||||
|
||||
(defcomp ~events-view-toggle (&key list-href tile-href hx-select list-active tile-active list-svg tile-svg)
|
||||
(div :class "hidden md:flex justify-end px-3 pt-3 gap-1"
|
||||
(a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class (str "p-1.5 rounded " list-active) :title "List view"
|
||||
:_ "on click js localStorage.removeItem('events_view') end"
|
||||
list-svg)
|
||||
(a :href tile-href :sx-get tile-href :sx-target "#main-panel" :sx-select hx-select
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class (str "p-1.5 rounded " tile-active) :title "Tile view"
|
||||
:_ "on click js localStorage.setItem('events_view','tile') end"
|
||||
tile-svg)))
|
||||
|
||||
(defcomp ~events-grid (&key grid-cls cards)
|
||||
(div :class grid-cls cards))
|
||||
|
||||
(defcomp ~events-empty ()
|
||||
(div :class "px-3 py-12 text-center text-stone-400"
|
||||
(i :class "fa fa-calendar-xmark text-4xl mb-3" :aria-hidden "true")
|
||||
(p :class "text-lg" "No upcoming events")))
|
||||
|
||||
(defcomp ~events-main-panel-body (&key toggle body)
|
||||
(<> toggle body (div :class "pb-8")))
|
||||
501
events/sx/forms.sx
Normal file
501
events/sx/forms.sx
Normal file
@@ -0,0 +1,501 @@
|
||||
;; 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)
|
||||
94
events/sx/fragments.sx
Normal file
94
events/sx/fragments.sx
Normal file
@@ -0,0 +1,94 @@
|
||||
;; Events fragment components — served as HTML fragments for other apps.
|
||||
;; container-cards entries, account page tickets, account page bookings.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Container cards entries (fragments/container_cards_entries.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-frag-entry-card (&key href name date-str time-str)
|
||||
(a :href href
|
||||
:class "flex flex-col gap-1 px-3 py-2 bg-stone-50 hover:bg-stone-100 rounded border border-stone-200 transition text-sm whitespace-nowrap flex-shrink-0 min-w-[180px]"
|
||||
(div :class "font-medium text-stone-900 truncate" name)
|
||||
(div :class "text-xs text-stone-600" date-str)
|
||||
(div :class "text-xs text-stone-500" time-str)))
|
||||
|
||||
(defcomp ~events-frag-entries-widget (&key cards)
|
||||
(div :class "mt-4 mb-2"
|
||||
(h3 :class "text-sm font-semibold text-stone-700 mb-2 px-2" "Events:")
|
||||
(div :class "overflow-x-auto scrollbar-hide" :style "scroll-behavior: smooth;"
|
||||
(div :class "flex gap-2 px-2" cards))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Account page tickets (fragments/account_page_tickets.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-frag-ticket-badge (&key state)
|
||||
(cond
|
||||
((= state "checked_in")
|
||||
(span :class "inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-2.5 py-0.5 text-xs font-medium text-blue-700" "checked in"))
|
||||
((= state "confirmed")
|
||||
(span :class "inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700" "confirmed"))
|
||||
(true
|
||||
(span :class "inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700" state))))
|
||||
|
||||
(defcomp ~events-frag-ticket-item (&key href entry-name date-str calendar-name type-name badge)
|
||||
(div :class "py-4 first:pt-0 last:pb-0"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "min-w-0 flex-1"
|
||||
(a :href href :class "text-sm font-medium text-stone-800 hover:text-emerald-700 transition"
|
||||
entry-name)
|
||||
(div :class "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500"
|
||||
(span date-str)
|
||||
calendar-name
|
||||
type-name))
|
||||
(div :class "flex-shrink-0" badge))))
|
||||
|
||||
(defcomp ~events-frag-tickets-panel (&key items)
|
||||
(div :class "w-full max-w-3xl mx-auto px-4 py-6"
|
||||
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
|
||||
(h1 :class "text-xl font-semibold tracking-tight" "Tickets")
|
||||
items)))
|
||||
|
||||
(defcomp ~events-frag-tickets-empty ()
|
||||
(p :class "text-sm text-stone-500" "No tickets yet."))
|
||||
|
||||
(defcomp ~events-frag-tickets-list (&key items)
|
||||
(div :class "divide-y divide-stone-100" items))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Account page bookings (fragments/account_page_bookings.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-frag-booking-badge (&key state)
|
||||
(cond
|
||||
((= state "confirmed")
|
||||
(span :class "inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700" "confirmed"))
|
||||
((= state "provisional")
|
||||
(span :class "inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700" "provisional"))
|
||||
(true
|
||||
(span :class "inline-flex items-center rounded-full bg-stone-50 border border-stone-200 px-2.5 py-0.5 text-xs font-medium text-stone-600" state))))
|
||||
|
||||
(defcomp ~events-frag-booking-item (&key name date-str calendar-name cost-str badge)
|
||||
(div :class "py-4 first:pt-0 last:pb-0"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "min-w-0 flex-1"
|
||||
(p :class "text-sm font-medium text-stone-800" name)
|
||||
(div :class "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500"
|
||||
(span date-str)
|
||||
calendar-name
|
||||
cost-str))
|
||||
(div :class "flex-shrink-0" badge))))
|
||||
|
||||
(defcomp ~events-frag-bookings-panel (&key items)
|
||||
(div :class "w-full max-w-3xl mx-auto px-4 py-6"
|
||||
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
|
||||
(h1 :class "text-xl font-semibold tracking-tight" "Bookings")
|
||||
items)))
|
||||
|
||||
(defcomp ~events-frag-bookings-empty ()
|
||||
(p :class "text-sm text-stone-500" "No bookings yet."))
|
||||
|
||||
(defcomp ~events-frag-bookings-list (&key items)
|
||||
(div :class "divide-y divide-stone-100" items))
|
||||
26
events/sx/header.sx
Normal file
26
events/sx/header.sx
Normal file
@@ -0,0 +1,26 @@
|
||||
;; Events header components
|
||||
|
||||
(defcomp ~events-calendars-label ()
|
||||
(<> (i :class "fa fa-calendar" :aria-hidden "true") (div "Calendars")))
|
||||
|
||||
(defcomp ~events-markets-label ()
|
||||
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div "Markets")))
|
||||
|
||||
(defcomp ~events-calendar-label (&key name description)
|
||||
(div :class "flex flex-col md:flex-row md:gap-2 items-center min-w-0"
|
||||
(div :class "flex flex-row items-center gap-2"
|
||||
(i :class "fa fa-calendar")
|
||||
(div :class "shrink-0" name))
|
||||
(div :id "calendar-description-title"
|
||||
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
description)))
|
||||
|
||||
(defcomp ~events-day-label (&key date-str)
|
||||
(div :class "flex gap-1 items-center"
|
||||
(i :class "fa fa-calendar-day")
|
||||
(span date-str)))
|
||||
|
||||
(defcomp ~events-entry-label (&key entry-id title times)
|
||||
(div :id (str "entry-title-" entry-id) :class "flex gap-1 items-center"
|
||||
title times))
|
||||
|
||||
39
events/sx/markets.sx
Normal file
39
events/sx/markets.sx
Normal file
@@ -0,0 +1,39 @@
|
||||
;; Events markets components
|
||||
|
||||
(defcomp ~events-markets-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
|
||||
:placeholder "e.g. Farm Shop, Bakery"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
|
||||
|
||||
(defcomp ~events-markets-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
form
|
||||
(div :id "markets-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~events-markets-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
|
||||
|
||||
(defcomp ~events-markets-item (&key href market-name market-slug del-url csrf-hdr)
|
||||
(div :class "mt-6 border rounded-lg p-4"
|
||||
(div :class "flex items-center justify-between gap-3"
|
||||
(a :class "flex items-baseline gap-3" :href href
|
||||
(h3 :class "font-semibold" market-name)
|
||||
(h4 :class "text-gray-500" (str "/" market-slug "/")))
|
||||
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
|
||||
:data-confirm true :data-confirm-title "Delete market?"
|
||||
:data-confirm-text "Products will be hidden (soft delete)"
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
386
events/sx/page.sx
Normal file
386
events/sx/page.sx
Normal file
@@ -0,0 +1,386 @@
|
||||
;; Events page-level components (slots, ticket types, buy form, cart, posts nav)
|
||||
|
||||
(defcomp ~events-slot-days-pills (&key days-inner)
|
||||
(div :class "flex flex-wrap gap-1" days-inner))
|
||||
|
||||
(defcomp ~events-slot-day-pill (&key day)
|
||||
(span :class "px-2 py-0.5 rounded-full text-xs bg-slate-200" day))
|
||||
|
||||
(defcomp ~events-slot-no-days ()
|
||||
(span :class "text-xs text-slate-400" "No days"))
|
||||
|
||||
(defcomp ~events-slot-panel (&key slot-id list-container days flexible time-str cost-str pre-action edit-url)
|
||||
(section :id (str "slot-" slot-id) :class list-container
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Days")
|
||||
(div :class "mt-1" days))
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Flexible")
|
||||
(div :class "mt-1" flexible))
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Time")
|
||||
(div :class "mt-1" time-str))
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Cost")
|
||||
(div :class "mt-1" cost-str)))
|
||||
(button :type "button" :class pre-action :sx-get edit-url
|
||||
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML" "Edit")))
|
||||
|
||||
(defcomp ~events-slot-description-oob (&key description)
|
||||
(div :id "slot-description-title" :sx-swap-oob "outerHTML"
|
||||
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
description))
|
||||
|
||||
(defcomp ~events-slots-empty-row ()
|
||||
(tr (td :colspan "5" :class "p-3 text-stone-500" "No slots yet.")))
|
||||
|
||||
(defcomp ~events-slots-row (&key tr-cls slot-href pill-cls hx-select slot-name description
|
||||
flexible days time-str cost-str action-btn del-url csrf-hdr)
|
||||
(tr :class tr-cls
|
||||
(td :class "p-2 align-top w-1/6"
|
||||
(div :class "font-medium"
|
||||
(a :href slot-href :class pill-cls :sx-get slot-href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" slot-name))
|
||||
(p :class "text-stone-500 whitespace-pre-line break-all w-full" description))
|
||||
(td :class "p-2 align-top w-1/6" flexible)
|
||||
(td :class "p-2 align-top w-1/6" days)
|
||||
(td :class "p-2 align-top w-1/6" time-str)
|
||||
(td :class "p-2 align-top w-1/6" cost-str)
|
||||
(td :class "p-2 align-top w-1/6"
|
||||
(button :class action-btn :type "button"
|
||||
:data-confirm "true" :data-confirm-title "Delete slot?"
|
||||
:data-confirm-text "This action cannot be undone."
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-target "#slots-table" :sx-select "#slots-table"
|
||||
:sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed"
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
(defcomp ~events-slots-table (&key list-container rows pre-action add-url)
|
||||
(section :id "slots-table" :class list-container
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
(tr (th :class "p-2 text-left w-1/6" "Name")
|
||||
(th :class "p-2 text-left w-1/6" "Flexible")
|
||||
(th :class "text-left p-2 w-1/6" "Days")
|
||||
(th :class "text-left p-2 w-1/6" "Time")
|
||||
(th :class "text-left p-2 w-1/6" "Cost")
|
||||
(th :class "text-left p-2 w-1/6" "Actions")))
|
||||
(tbody rows))
|
||||
(div :id "slot-add-container" :class "mt-4"
|
||||
(button :type "button" :class pre-action
|
||||
:sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
|
||||
"+ Add slot"))))
|
||||
|
||||
(defcomp ~events-ticket-type-col (&key label value)
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label)
|
||||
(div :class "mt-1" value)))
|
||||
|
||||
(defcomp ~events-ticket-type-panel (&key ticket-id list-container c1 c2 c3 pre-action edit-url)
|
||||
(section :id (str "ticket-" ticket-id) :class list-container
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm"
|
||||
c1 c2 c3)
|
||||
(button :type "button" :class pre-action :sx-get edit-url
|
||||
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML" "Edit")))
|
||||
|
||||
(defcomp ~events-ticket-types-empty-row ()
|
||||
(tr (td :colspan "4" :class "p-3 text-stone-500" "No ticket types yet.")))
|
||||
|
||||
(defcomp ~events-ticket-types-row (&key tr-cls tt-href pill-cls hx-select tt-name cost-str count
|
||||
action-btn del-url csrf-hdr)
|
||||
(tr :class tr-cls
|
||||
(td :class "p-2 align-top w-1/3"
|
||||
(div :class "font-medium"
|
||||
(a :href tt-href :class pill-cls :sx-get tt-href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" tt-name)))
|
||||
(td :class "p-2 align-top w-1/4" cost-str)
|
||||
(td :class "p-2 align-top w-1/4" count)
|
||||
(td :class "p-2 align-top w-1/6"
|
||||
(button :class action-btn :type "button"
|
||||
:data-confirm "true" :data-confirm-title "Delete ticket type?"
|
||||
:data-confirm-text "This action cannot be undone."
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-target "#tickets-table" :sx-select "#tickets-table"
|
||||
:sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed"
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
(defcomp ~events-ticket-types-table (&key list-container rows action-btn add-url)
|
||||
(section :id "tickets-table" :class list-container
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
(tr (th :class "p-2 text-left w-1/3" "Name")
|
||||
(th :class "text-left p-2 w-1/4" "Cost")
|
||||
(th :class "text-left p-2 w-1/4" "Count")
|
||||
(th :class "text-left p-2 w-1/6" "Actions")))
|
||||
(tbody rows))
|
||||
(div :id "ticket-add-container" :class "mt-4"
|
||||
(button :class action-btn :sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
|
||||
(i :class "fa fa-plus") " Add ticket type"))))
|
||||
|
||||
(defcomp ~events-ticket-config-display (&key price-str count-str show-js)
|
||||
(div :class "space-y-2"
|
||||
(div :class "flex items-center gap-2"
|
||||
(span :class "text-sm font-medium text-stone-700" "Price:")
|
||||
(span :class "font-medium text-green-600" price-str))
|
||||
(div :class "flex items-center gap-2"
|
||||
(span :class "text-sm font-medium text-stone-700" "Available:")
|
||||
(span :class "font-medium text-blue-600" count-str))
|
||||
(button :type "button" :class "text-xs text-blue-600 hover:text-blue-800 underline"
|
||||
:onclick show-js "Edit ticket config")))
|
||||
|
||||
(defcomp ~events-ticket-config-none (&key show-js)
|
||||
(div :class "space-y-2"
|
||||
(span :class "text-sm text-stone-400" "No tickets configured")
|
||||
(button :type "button" :class "block text-xs text-blue-600 hover:text-blue-800 underline"
|
||||
:onclick show-js "Configure tickets")))
|
||||
|
||||
(defcomp ~events-ticket-config-form (&key entry-id hidden-cls update-url csrf price-val count-val hide-js)
|
||||
(form :id (str "ticket-form-" entry-id) :class (str hidden-cls " space-y-3 mt-2 p-3 border rounded bg-stone-50")
|
||||
:sx-post update-url :sx-target (str "#entry-tickets-" entry-id) :sx-swap "innerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div (label :for (str "ticket-price-" entry-id) :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
"Ticket Price (£)")
|
||||
(input :type "number" :id (str "ticket-price-" entry-id) :name "ticket_price"
|
||||
:step "0.01" :min "0" :value price-val
|
||||
:class "w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
:placeholder "e.g., 5.00"))
|
||||
(div (label :for (str "ticket-count-" entry-id) :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
"Total Tickets")
|
||||
(input :type "number" :id (str "ticket-count-" entry-id) :name "ticket_count"
|
||||
:min "0" :value count-val
|
||||
:class "w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
:placeholder "Leave empty for unlimited"))
|
||||
(div :class "flex gap-2"
|
||||
(button :type "submit" :class "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm" "Save")
|
||||
(button :type "button" :class "px-4 py-2 bg-stone-200 text-stone-700 rounded hover:bg-stone-300 text-sm"
|
||||
:onclick hide-js "Cancel"))))
|
||||
|
||||
(defcomp ~events-buy-not-confirmed (&key entry-id)
|
||||
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-stone-50 p-4 text-sm text-stone-500"
|
||||
(i :class "fa fa-ticket mr-1" :aria-hidden "true")
|
||||
"Tickets available once this event is confirmed."))
|
||||
|
||||
(defcomp ~events-buy-info-sold (&key count)
|
||||
(span (str count " sold")))
|
||||
|
||||
(defcomp ~events-buy-info-remaining (&key count)
|
||||
(span (str count " remaining")))
|
||||
|
||||
(defcomp ~events-buy-info-basket (&key count)
|
||||
(span :class "text-emerald-600 font-medium"
|
||||
(i :class "fa fa-shopping-cart text-[0.6rem]" :aria-hidden "true")
|
||||
(str " " count " in basket")))
|
||||
|
||||
(defcomp ~events-buy-info-bar (&key items)
|
||||
(div :class "flex items-center gap-3 mb-3 text-xs text-stone-500" items))
|
||||
|
||||
(defcomp ~events-buy-type-item (&key type-name cost-str adjust-controls)
|
||||
(div :class "flex items-center justify-between p-3 rounded-lg bg-stone-50 border border-stone-100"
|
||||
(div (div :class "font-medium text-sm" type-name)
|
||||
(div :class "text-xs text-stone-500" cost-str))
|
||||
adjust-controls))
|
||||
|
||||
(defcomp ~events-buy-types-wrapper (&key items)
|
||||
(div :class "space-y-2" items))
|
||||
|
||||
(defcomp ~events-buy-default (&key price-str adjust-controls)
|
||||
(<> (div :class "flex items-center justify-between mb-4"
|
||||
(div (span :class "font-medium text-green-600" price-str)
|
||||
(span :class "text-sm text-stone-500 ml-2" "per ticket")))
|
||||
adjust-controls))
|
||||
|
||||
(defcomp ~events-buy-panel (&key entry-id info body)
|
||||
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-white p-4"
|
||||
(h3 :class "text-sm font-semibold text-stone-700 mb-3"
|
||||
(i :class "fa fa-ticket mr-1" :aria-hidden "true") "Tickets")
|
||||
info body))
|
||||
|
||||
(defcomp ~events-adjust-form (&key adjust-url target extra-cls csrf entry-id tt count-val btn)
|
||||
(form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" :class extra-cls
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
tt
|
||||
(input :type "hidden" :name "count" :value count-val)
|
||||
btn))
|
||||
|
||||
(defcomp ~events-adjust-tt-hidden (&key ticket-type-id)
|
||||
(input :type "hidden" :name "ticket_type_id" :value ticket-type-id))
|
||||
|
||||
(defcomp ~events-adjust-cart-plus ()
|
||||
(button :type "submit"
|
||||
:class "relative inline-flex items-center justify-center text-sm font-medium text-stone-500 hover:bg-emerald-50 rounded p-1"
|
||||
(i :class "fa fa-cart-plus text-2xl" :aria-hidden "true")))
|
||||
|
||||
(defcomp ~events-adjust-minus ()
|
||||
(button :type "submit"
|
||||
:class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl"
|
||||
"-"))
|
||||
|
||||
(defcomp ~events-adjust-plus ()
|
||||
(button :type "submit"
|
||||
:class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl"
|
||||
"+"))
|
||||
|
||||
(defcomp ~events-adjust-cart-icon (&key href count)
|
||||
(a :class "relative inline-flex items-center justify-center text-emerald-700" :href href
|
||||
(span :class "relative inline-flex items-center justify-center"
|
||||
(i :class "fa-solid fa-shopping-cart text-2xl" :aria-hidden "true")
|
||||
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||
(span :class "flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold" count)))))
|
||||
|
||||
(defcomp ~events-adjust-controls (&key minus cart-icon plus)
|
||||
(div :class "flex items-center gap-2" minus cart-icon plus))
|
||||
|
||||
(defcomp ~events-buy-result (&key entry-id count-label tickets remaining my-tickets-href)
|
||||
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-emerald-200 bg-emerald-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-3"
|
||||
(i :class "fa fa-check-circle text-emerald-600" :aria-hidden "true")
|
||||
(span :class "font-semibold text-emerald-800" count-label))
|
||||
(div :class "space-y-2 mb-4" tickets)
|
||||
remaining
|
||||
(div :class "mt-3 flex gap-2"
|
||||
(a :href my-tickets-href :class "text-sm text-emerald-700 hover:text-emerald-900 underline"
|
||||
"View all my tickets"))))
|
||||
|
||||
(defcomp ~events-buy-result-ticket (&key href code-short)
|
||||
(a :href href :class "flex items-center justify-between p-2 rounded-lg bg-white border border-emerald-100 hover:border-emerald-300 transition text-sm"
|
||||
(div :class "flex items-center gap-2"
|
||||
(i :class "fa fa-ticket text-emerald-500" :aria-hidden "true")
|
||||
(span :class "font-mono text-xs text-stone-500" code-short))
|
||||
(span :class "text-xs text-emerald-600 font-medium" "View ticket")))
|
||||
|
||||
(defcomp ~events-buy-result-remaining (&key text)
|
||||
(p :class "text-xs text-stone-500" text))
|
||||
|
||||
(defcomp ~events-cart-icon-logo (&key blog-href logo)
|
||||
(div :id "cart-mini" :sx-swap-oob "true"
|
||||
(div :class "h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0"
|
||||
(a :href blog-href :class "h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
|
||||
(img :src logo :class "h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0")))))
|
||||
|
||||
(defcomp ~events-cart-icon-badge (&key cart-href count)
|
||||
(div :id "cart-mini" :sx-swap-oob "true"
|
||||
(a :href cart-href :class "relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
|
||||
(i :class "fa fa-shopping-cart text-5xl" :aria-hidden "true")
|
||||
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
|
||||
count))))
|
||||
|
||||
;; Inline ticket widget (for all-events/page-summary cards)
|
||||
(defcomp ~events-tw-form (&key ticket-url target csrf entry-id count-val btn)
|
||||
(form :action ticket-url :method "post" :sx-post ticket-url :sx-target target :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
(input :type "hidden" :name "count" :value count-val)
|
||||
btn))
|
||||
|
||||
(defcomp ~events-tw-cart-plus ()
|
||||
(button :type "submit" :class "relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1"
|
||||
(i :class "fa fa-cart-plus text-2xl" :aria-hidden "true")))
|
||||
|
||||
(defcomp ~events-tw-minus ()
|
||||
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "-"))
|
||||
|
||||
(defcomp ~events-tw-plus ()
|
||||
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+"))
|
||||
|
||||
(defcomp ~events-tw-cart-icon (&key qty)
|
||||
(span :class "relative inline-flex items-center justify-center text-emerald-700"
|
||||
(span :class "relative inline-flex items-center justify-center"
|
||||
(i :class "fa-solid fa-shopping-cart text-xl" :aria-hidden "true")
|
||||
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||
(span :class "flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold" qty)))))
|
||||
|
||||
(defcomp ~events-tw-widget (&key entry-id price inner)
|
||||
(div :id (str "page-ticket-" entry-id) :class "flex items-center gap-2"
|
||||
(span :class "text-green-600 font-medium text-sm" price)
|
||||
inner))
|
||||
|
||||
;; Entry posts panel
|
||||
(defcomp ~events-entry-posts-panel (&key posts search-url entry-id)
|
||||
(div :class "space-y-2"
|
||||
posts
|
||||
(div :class "mt-3 pt-3 border-t"
|
||||
(label :class "block text-xs font-medium text-stone-700 mb-1" "Add Post")
|
||||
(input :type "text" :placeholder "Search posts..."
|
||||
:class "w-full px-3 py-2 border rounded text-sm"
|
||||
:sx-get search-url :sx-trigger "keyup changed delay:300ms, load"
|
||||
:sx-target (str "#post-search-results-" entry-id) :sx-swap "innerHTML" :name "q")
|
||||
(div :id (str "post-search-results-" entry-id) :class "mt-2 max-h-96 overflow-y-auto border rounded"))))
|
||||
|
||||
(defcomp ~events-entry-posts-list (&key items)
|
||||
(div :class "space-y-2" items))
|
||||
|
||||
(defcomp ~events-entry-posts-none ()
|
||||
(p :class "text-sm text-stone-400" "No posts associated"))
|
||||
|
||||
(defcomp ~events-entry-post-item (&key img title del-url entry-id csrf-hdr)
|
||||
(div :class "flex items-center justify-between gap-3 p-2 bg-stone-50 rounded border"
|
||||
img (span :class "text-sm flex-1" title)
|
||||
(button :type "button" :class "text-xs text-red-600 hover:text-red-800 flex-shrink-0"
|
||||
:data-confirm "true" :data-confirm-title "Remove post?"
|
||||
:data-confirm-text (str "This will remove " title " from this entry")
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, remove it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa fa-times") " Remove")))
|
||||
|
||||
(defcomp ~events-post-img (&key src alt)
|
||||
(img :src src :alt alt :class "w-8 h-8 rounded-full object-cover flex-shrink-0"))
|
||||
|
||||
(defcomp ~events-post-img-placeholder ()
|
||||
(div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"))
|
||||
|
||||
;; Entry posts nav OOB
|
||||
(defcomp ~events-entry-posts-nav-oob-empty ()
|
||||
(div :id "entry-posts-nav-wrapper" :sx-swap-oob "true"))
|
||||
|
||||
(defcomp ~events-entry-posts-nav-oob (&key items)
|
||||
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
|
||||
:id "entry-posts-nav-wrapper" :sx-swap-oob "true"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
|
||||
|
||||
(defcomp ~events-entry-nav-post (&key href nav-btn img title)
|
||||
(a :href href :class nav-btn img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
|
||||
|
||||
;; Post nav entries OOB
|
||||
(defcomp ~events-post-nav-oob-empty ()
|
||||
(div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true"))
|
||||
|
||||
(defcomp ~events-post-nav-entry (&key href nav-btn name time-str)
|
||||
(a :href href :class nav-btn
|
||||
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" time-str))))
|
||||
|
||||
(defcomp ~events-post-nav-calendar (&key href nav-btn name)
|
||||
(a :href href :class nav-btn
|
||||
(i :class "fa fa-calendar" :aria-hidden "true")
|
||||
(div name)))
|
||||
|
||||
(defcomp ~events-post-nav-wrapper (&key items hyperscript)
|
||||
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
|
||||
:id "entries-calendars-nav-wrapper" :sx-swap-oob "true"
|
||||
(button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
:aria-label "Scroll left"
|
||||
:_ "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft - 200"
|
||||
(i :class "fa fa-chevron-left"))
|
||||
(div :id "associated-items-container"
|
||||
:class "overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none"
|
||||
:style "scroll-behavior: smooth;" :_ hyperscript
|
||||
(div :class "flex flex-col sm:flex-row gap-1" items))
|
||||
(style ".scrollbar-hide::-webkit-scrollbar { display: none; } .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }")
|
||||
(button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
:aria-label "Scroll right"
|
||||
:_ "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200"
|
||||
(i :class "fa fa-chevron-right"))))
|
||||
|
||||
;; Entry nav post link (with image)
|
||||
(defcomp ~events-entry-nav-post-link (&key href img title)
|
||||
(a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"
|
||||
img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
|
||||
59
events/sx/payments.sx
Normal file
59
events/sx/payments.sx
Normal file
@@ -0,0 +1,59 @@
|
||||
;; Events payments components
|
||||
|
||||
(defcomp ~events-payments-panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix)
|
||||
(section :class "p-4 max-w-lg mx-auto"
|
||||
(div :id "payments-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"
|
||||
(h3 :class "text-lg font-semibold text-stone-800"
|
||||
(i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")
|
||||
(p :class "text-xs text-stone-400" "Configure per-page SumUp credentials. Leave blank to use the global merchant account.")
|
||||
(form :sx-put update-url :sx-target "#payments-panel" :sx-swap "outerHTML" :sx-select "#payments-panel" :class "space-y-3"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Merchant Code")
|
||||
(input :type "text" :name "merchant_code" :value merchant-code :placeholder "e.g. ME4J6100" :class input-cls))
|
||||
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "API Key")
|
||||
(input :type "password" :name "api_key" :value "" :placeholder placeholder :class input-cls)
|
||||
(when sumup-configured (p :class "text-xs text-stone-400 mt-0.5" "Key is set. Leave blank to keep current key.")))
|
||||
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Checkout Reference Prefix")
|
||||
(input :type "text" :name "checkout_prefix" :value checkout-prefix :placeholder "e.g. ROSE-" :class input-cls))
|
||||
(button :type "submit" :class "px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"
|
||||
"Save SumUp Settings")
|
||||
(when sumup-configured (span :class "ml-2 text-xs text-green-600"
|
||||
(i :class "fa fa-check-circle") " Connected"))))))
|
||||
|
||||
(defcomp ~events-markets-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
|
||||
:placeholder "e.g. Farm Shop, Bakery"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
|
||||
|
||||
(defcomp ~events-markets-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
form
|
||||
(div :id "markets-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~events-markets-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
|
||||
|
||||
(defcomp ~events-markets-item (&key href market-name market-slug del-url csrf-hdr)
|
||||
(div :class "mt-6 border rounded-lg p-4"
|
||||
(div :class "flex items-center justify-between gap-3"
|
||||
(a :class "flex items-baseline gap-3" :href href
|
||||
(h3 :class "font-semibold" market-name)
|
||||
(h4 :class "text-gray-500" (str "/" market-slug "/")))
|
||||
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
|
||||
:data-confirm true :data-confirm-title "Delete market?"
|
||||
:data-confirm-text "Products will be hidden (soft delete)"
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
3512
events/sx/sx_components.py
Normal file
3512
events/sx/sx_components.py
Normal file
File diff suppressed because it is too large
Load Diff
206
events/sx/tickets.sx
Normal file
206
events/sx/tickets.sx
Normal file
@@ -0,0 +1,206 @@
|
||||
;; Events ticket components
|
||||
|
||||
(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"
|
||||
(div :class "font-semibold text-lg truncate" entry-name)
|
||||
(when type-name (div :class "text-sm text-stone-600 mt-0.5" type-name))
|
||||
(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"
|
||||
badge
|
||||
(span :class "text-xs text-stone-400 font-mono" (str code-prefix "..."))))))
|
||||
|
||||
(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" 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
|
||||
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")
|
||||
(a :href back-href :class "inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4"
|
||||
(i :class "fa fa-arrow-left" :aria-hidden "true") " Back to my tickets")
|
||||
(div :class "rounded-2xl border border-stone-200 bg-white overflow-hidden"
|
||||
(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)
|
||||
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")
|
||||
(p :class "text-xs text-stone-400 mt-3 font-mono select-all" code))
|
||||
(div :class "px-6 py-4 space-y-3"
|
||||
(when time-date (div :class "flex items-start gap-3"
|
||||
(i :class "fa fa-calendar text-stone-400 mt-0.5" :aria-hidden "true")
|
||||
(div (div :class "text-sm font-medium" time-date)
|
||||
(div :class "text-sm text-stone-500" time-range))))
|
||||
(when cal-name (div :class "flex items-start gap-3"
|
||||
(i :class "fa fa-map-pin text-stone-400 mt-0.5" :aria-hidden "true")
|
||||
(div :class "text-sm" cal-name)))
|
||||
(when type-desc (div :class "flex items-start gap-3"
|
||||
(i :class "fa fa-tag text-stone-400 mt-0.5" :aria-hidden "true")
|
||||
(div :class "text-sm" type-desc)))
|
||||
(when checkin-str (div :class "flex items-start gap-3"
|
||||
(i :class "fa fa-check-circle text-blue-500 mt-0.5" :aria-hidden "true")
|
||||
(div :class "text-sm text-blue-700" checkin-str)))))
|
||||
(script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js")
|
||||
(script qr-script)))
|
||||
|
||||
(defcomp ~events-ticket-admin-stat (&key border bg text-cls label-cls value label)
|
||||
(div :class (str "rounded-xl border " border " " bg " p-4 text-center")
|
||||
(div :class (str "text-2xl font-bold " text-cls) value)
|
||||
(div :class (str "text-xs " label-cls " uppercase tracking-wide") label)))
|
||||
|
||||
(defcomp ~events-ticket-admin-date (&key date-str)
|
||||
(div :class "text-xs text-stone-500" date-str))
|
||||
|
||||
(defcomp ~events-ticket-admin-checkin-form (&key checkin-url code csrf)
|
||||
(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")))
|
||||
|
||||
(defcomp ~events-ticket-admin-checked-in (&key time-str)
|
||||
(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 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) date)
|
||||
(td :class "px-4 py-3 text-sm" type-name)
|
||||
(td :class "px-4 py-3" badge)
|
||||
(td :class "px-4 py-3" action)))
|
||||
|
||||
(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" 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")
|
||||
(div :class "flex gap-3 mb-4"
|
||||
(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"
|
||||
: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'))"
|
||||
(i :class "fa fa-search" :aria-hidden "true")))
|
||||
(div :id "lookup-result"
|
||||
(div :class "text-sm text-stone-400 text-center py-4" "Enter a ticket code to look it up")))
|
||||
(div :class "rounded-xl border border-stone-200 bg-white overflow-hidden"
|
||||
(h2 :class "text-lg font-semibold px-6 py-4 border-b border-stone-100" "Recent Tickets")
|
||||
(if has-tickets
|
||||
(div :class "overflow-x-auto"
|
||||
(table :class "w-full text-sm"
|
||||
(thead :class "bg-stone-50"
|
||||
(tr (th :class "px-4 py-3 text-left font-medium text-stone-600" "Code")
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "Event")
|
||||
(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" 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 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) date)
|
||||
(td :class "px-4 py-3 text-sm" type-name)
|
||||
(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)))))
|
||||
|
||||
(defcomp ~events-lookup-error (&key message)
|
||||
(div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800"
|
||||
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
|
||||
|
||||
(defcomp ~events-lookup-info (&key entry-name)
|
||||
(div :class "font-semibold text-lg" entry-name))
|
||||
|
||||
(defcomp ~events-lookup-type (&key type-name)
|
||||
(div :class "text-sm text-stone-600" type-name))
|
||||
|
||||
(defcomp ~events-lookup-date (&key date-str)
|
||||
(div :class "text-sm text-stone-500 mt-1" date-str))
|
||||
|
||||
(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 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 :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"
|
||||
(i :class "fa fa-check mr-2" :aria-hidden "true") "Check In")))
|
||||
|
||||
(defcomp ~events-lookup-checked-in ()
|
||||
(div :class "text-blue-600 text-center"
|
||||
(i :class "fa fa-check-circle text-3xl" :aria-hidden "true")
|
||||
(div :class "text-sm font-medium mt-1" "Checked In")))
|
||||
|
||||
(defcomp ~events-lookup-cancelled ()
|
||||
(div :class "text-red-600 text-center"
|
||||
(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 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" info)
|
||||
(div :id (str "checkin-action-" code) action))))
|
||||
|
||||
(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" badge)
|
||||
(td :class "px-4 py-2" action)))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-checkin (&key checkin-url code csrf)
|
||||
(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)
|
||||
(div :class "overflow-x-auto rounded-xl border border-stone-200"
|
||||
(table :class "w-full text-sm"
|
||||
(thead :class "bg-stone-50"
|
||||
(tr (th :class "px-4 py-2 text-left font-medium text-stone-600" "Code")
|
||||
(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" 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)
|
||||
(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))
|
||||
body))
|
||||
Reference in New Issue
Block a user