Files
rose-ash/events/sx/tickets.sx
giles c0665ba58e Adopt Step 7 language features across SX codebase
112 conversions across 19 .sx files using match, let-match, and pipe operators:

match (17): type/value dispatch replacing cond/if chains
  - lib/vm.sx: HO form dispatch (for-each/map/filter/reduce/some/every?)
  - lib/tree-tools.sx: node-display, node-matches?, rename, count, replace, free-symbols
  - lib/types.sx: narrow-type, substitute-in-type, infer-type, resolve-type
  - web/engine.sx: default-trigger, resolve-target, classify-trigger
  - web/deps.sx: scan-refs-walk, scan-io-refs-walk

let-match (89): dict destructuring replacing (get d "key") patterns
  - shared/page-functions.sx (20), blog/admin.sx (17), pub-api.sx (13)
  - events/ layouts/page/tickets/entries/forms (27 total)
  - specs-explorer.sx (7), federation/social.sx (3), lib/ small files (3)

-> pipes (6): replacing triple-chained gets in lib/vm.sx
  - frame-closure → closure-code → code-bytecode chains

Also: lib/vm.sx accessor upgrades (get vm "sp" → vm-sp vm throughout)

2650/2650 tests pass, zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:49:02 +00:00

440 lines
21 KiB
Plaintext

;; 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)
(let-match
{:time-str time-str :href href :type-name type-name :code-prefix code-prefix :entry-name entry-name :cal-name cal-name :state state}
t
(~tickets/card
:href href
:entry-name entry-name
:type-name type-name
:time-str time-str
:cal-name cal-name
:badge (~entries/ticket-state-badge :state state)
:code-prefix 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)
(let-match
{:date-str date-str :csrf csrf :type-name type-name :code-short code-short :entry-name entry-name :code code :checkin-url checkin-url :checked-in-time checked-in-time :state state}
t
(~tickets/admin-row-from-data
:code code
:code-short code-short
:entry-name entry-name
:date-str date-str
:type-name type-name
:state state
:checkin-url checkin-url
:csrf csrf
:checked-in-time 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)
(let-match
{:type-name type-name :code-short code-short :code code :checkin-url checkin-url :checked-in-time checked-in-time :state state}
t
(~tickets/entry-tickets-admin-row
:code code
:code-short code-short
:type-name type-name
:badge (~entries/ticket-state-badge :state state)
:action (cond
((or (= state "confirmed") (= state "paid"))
(~tickets/entry-tickets-admin-checkin
:checkin-url checkin-url
:code code
:csrf csrf))
((= state "checked-in")
(~tickets/admin-checked-in
:time-str (or 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)
(let-match
{:tt-href tt-href :count count :cost-str cost-str :tt-name tt-name :del-url del-url}
tt
(~page/ticket-types-row
:tr-cls tr-cls
:tt-href tt-href
:pill-cls pill-cls
:hx-select hx-select
:tt-name tt-name
:cost-str cost-str
:count count
:action-btn action-btn
:del-url 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))))