;; Events ticket components (defcomp ~tickets/card (&key (href :as string) (entry-name :as string) (type-name :as string?) (time-str :as string?) (cal-name :as string?) badge (code-prefix :as string)) (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 ~tickets/panel (&key (list-container :as string) (has-tickets :as boolean) 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 ~tickets/detail (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) badge (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string)) (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 ~tickets/admin-stat (&key (border :as string) (bg :as string) (text-cls :as string) (label-cls :as string) (value :as string) (label :as string)) (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 ~tickets/admin-date (&key (date-str :as string)) (div :class "text-xs text-stone-500" date-str)) (defcomp ~tickets/admin-checkin-form (&key (checkin-url :as string) (code :as string) (csrf :as string)) (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 ~tickets/admin-checked-in (&key (time-str :as string)) (span :class "text-xs text-blue-600" (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))) (defcomp ~tickets/admin-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) 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 ~tickets/admin-panel (&key (list-container :as string) stats (lookup-url :as string) (has-tickets :as boolean) 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 ~tickets/checkin-error (&key (message :as string)) (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 ~tickets/checkin-success-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge (time-str :as string)) (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 ~tickets/lookup-error (&key (message :as string)) (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 ~tickets/lookup-info (&key (entry-name :as string)) (div :class "font-semibold text-lg" entry-name)) (defcomp ~tickets/lookup-type (&key (type-name :as string)) (div :class "text-sm text-stone-600" type-name)) (defcomp ~tickets/lookup-date (&key (date-str :as string)) (div :class "text-sm text-stone-500 mt-1" date-str)) (defcomp ~tickets/lookup-cal (&key (cal-name :as string)) (div :class "text-xs text-stone-400 mt-0.5" cal-name)) (defcomp ~tickets/lookup-status (&key badge (code :as string)) (div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code))) (defcomp ~tickets/lookup-checkin-time (&key (date-str :as string)) (div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str))) (defcomp ~tickets/lookup-checkin-btn (&key (checkin-url :as string) (code :as string) (csrf :as string)) (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 ~tickets/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 ~tickets/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 ~tickets/lookup-card (&key info (code :as string) 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 ~tickets/entry-tickets-admin-row (&key (code :as string) (code-short :as string) (type-name :as string) 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 ~tickets/entry-tickets-admin-checkin (&key (checkin-url :as string) (code :as string) (csrf :as string)) (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 ~tickets/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 ~tickets/entry-tickets-admin-empty () (div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry")) (defcomp ~tickets/entry-tickets-admin-panel (&key (entry-name :as string) (count-label :as string) 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)) ;; --------------------------------------------------------------------------- ;; Composition defcomps — receive data, compose ticket trees ;; --------------------------------------------------------------------------- ;; My tickets panel from data (defcomp ~tickets/panel-from-data (&key (list-container :as string) (tickets :as list?)) (~tickets/panel :list-container list-container :has-tickets (not (empty? (or tickets (list)))) :cards (<> (map (lambda (t) (~tickets/card :href (get t "href") :entry-name (get t "entry-name") :type-name (get t "type-name") :time-str (get t "time-str") :cal-name (get t "cal-name") :badge (~entries/ticket-state-badge :state (get t "state")) :code-prefix (get t "code-prefix"))) (or tickets (list)))))) ;; Ticket detail from data — uses lg badge variant (defcomp ~tickets/detail-from-data (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) (state :as string) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string)) (~tickets/detail :list-container list-container :back-href back-href :header-bg header-bg :entry-name entry-name :badge (~entries/ticket-state-badge-lg :state state) :type-name type-name :code code :time-date time-date :time-range time-range :cal-name cal-name :type-desc type-desc :checkin-str checkin-str :qr-script qr-script)) ;; Ticket admin row from data — conditional action column (defcomp ~tickets/admin-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (type-name :as string) (state :as string) (checkin-url :as string) (csrf :as string) (checked-in-time :as string?)) (~tickets/admin-row :code code :code-short code-short :entry-name entry-name :date (when date-str (~tickets/admin-date :date-str date-str)) :type-name type-name :badge (~entries/ticket-state-badge :state state) :action (cond ((or (= state "confirmed") (= state "reserved")) (~tickets/admin-checkin-form :checkin-url checkin-url :code code :csrf csrf)) ((= state "checked_in") (~tickets/admin-checked-in :time-str (or checked-in-time ""))) (true nil)))) ;; Ticket admin panel from data (defcomp ~tickets/admin-panel-from-data (&key (list-container :as string) (lookup-url :as string) (tickets :as list?) (total :as number?) (confirmed :as number?) (checked-in :as number?) (reserved :as number?)) (~tickets/admin-panel :list-container list-container :stats (<> (~tickets/admin-stat :border "border-stone-200" :bg "" :text-cls "text-stone-900" :label-cls "text-stone-500" :value (str (or total 0)) :label "Total") (~tickets/admin-stat :border "border-emerald-200" :bg "bg-emerald-50" :text-cls "text-emerald-700" :label-cls "text-emerald-600" :value (str (or confirmed 0)) :label "Confirmed") (~tickets/admin-stat :border "border-blue-200" :bg "bg-blue-50" :text-cls "text-blue-700" :label-cls "text-blue-600" :value (str (or checked-in 0)) :label "Checked In") (~tickets/admin-stat :border "border-amber-200" :bg "bg-amber-50" :text-cls "text-amber-700" :label-cls "text-amber-600" :value (str (or reserved 0)) :label "Reserved")) :lookup-url lookup-url :has-tickets (not (empty? (or tickets (list)))) :rows (<> (map (lambda (t) (~tickets/admin-row-from-data :code (get t "code") :code-short (get t "code-short") :entry-name (get t "entry-name") :date-str (get t "date-str") :type-name (get t "type-name") :state (get t "state") :checkin-url (get t "checkin-url") :csrf (get t "csrf") :checked-in-time (get t "checked-in-time"))) (or tickets (list)))))) ;; Entry tickets admin from data (defcomp ~tickets/entry-tickets-admin-from-data (&key (entry-name :as string) (count-label :as string) (tickets :as list?) (csrf :as string)) (~tickets/entry-tickets-admin-panel :entry-name entry-name :count-label count-label :body (if (empty? (or tickets (list))) (~tickets/entry-tickets-admin-empty) (~tickets/entry-tickets-admin-table :rows (<> (map (lambda (t) (~tickets/entry-tickets-admin-row :code (get t "code") :code-short (get t "code-short") :type-name (get t "type-name") :badge (~entries/ticket-state-badge :state (get t "state")) :action (cond ((or (= (get t "state") "confirmed") (= (get t "state") "reserved")) (~tickets/entry-tickets-admin-checkin :checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf)) ((= (get t "state") "checked_in") (~tickets/admin-checked-in :time-str (or (get t "checked-in-time") ""))) (true nil)))) (or tickets (list)))))))) ;; Checkin success row from data (defcomp ~tickets/checkin-success-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (type-name :as string) (time-str :as string)) (~tickets/checkin-success-row :code code :code-short code-short :entry-name entry-name :date (when date-str (~tickets/admin-date :date-str date-str)) :type-name type-name :badge (~entries/ticket-state-badge :state "checked_in") :time-str time-str)) ;; Ticket types table from data (defcomp ~tickets/types-table-from-data (&key (list-container :as string) (ticket-types :as list?) (action-btn :as string) (add-url :as string) (tr-cls :as string) (pill-cls :as string) (hx-select :as string) (csrf-hdr :as string)) (~page/ticket-types-table :list-container list-container :rows (if (empty? (or ticket-types (list))) (~page/ticket-types-empty-row) (<> (map (lambda (tt) (~page/ticket-types-row :tr-cls tr-cls :tt-href (get tt "tt-href") :pill-cls pill-cls :hx-select hx-select :tt-name (get tt "tt-name") :cost-str (get tt "cost-str") :count (get tt "count") :action-btn action-btn :del-url (get tt "del-url") :csrf-hdr csrf-hdr)) (or ticket-types (list))))) :action-btn action-btn :add-url add-url)) ;; Lookup result from data (defcomp ~tickets/lookup-result-from-data (&key (entry-name :as string) (type-name :as string?) (date-str :as string?) (cal-name :as string?) (state :as string) (code :as string) (checked-in-str :as string?) (checkin-url :as string) (csrf :as string)) (~tickets/lookup-card :info (<> (~tickets/lookup-info :entry-name entry-name) (when type-name (~tickets/lookup-type :type-name type-name)) (when date-str (~tickets/lookup-date :date-str date-str)) (when cal-name (~tickets/lookup-cal :cal-name cal-name)) (~tickets/lookup-status :badge (~entries/ticket-state-badge :state state) :code code) (when checked-in-str (~tickets/lookup-checkin-time :date-str checked-in-str))) :code code :action (cond ((or (= state "confirmed") (= state "reserved")) (~tickets/lookup-checkin-btn :checkin-url checkin-url :code code :csrf csrf)) ((= state "checked_in") (~tickets/lookup-checked-in)) ((= state "cancelled") (~tickets/lookup-cancelled)) (true nil))))