Move events/market/blog composition from Python to .sx defcomps (Phase 9)
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m33s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m33s
Continues the pattern of eliminating Python sx_call tree-building in favour of data-driven .sx defcomps. POST/PUT/DELETE routes now pass plain data (dicts, lists, scalars) and let .sx handle iteration, conditionals, and layout via map/let/when/if. Single response components wrap OOB swaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,36 @@
|
||||
(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))))
|
||||
|
||||
;; Calendar grid from data — all iteration in sx
|
||||
(defcomp ~events-calendar-grid-from-data (&key pill-cls month-name year
|
||||
prev-year-href prev-month-href
|
||||
next-month-href next-year-href
|
||||
weekday-names cells)
|
||||
(~events-calendar-grid
|
||||
:arrows (<>
|
||||
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab")
|
||||
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-month-href :label "\u2039")
|
||||
(~events-calendar-month-label :month-name month-name :year year)
|
||||
(~events-calendar-nav-arrow :pill-cls pill-cls :href next-month-href :label "\u203a")
|
||||
(~events-calendar-nav-arrow :pill-cls pill-cls :href next-year-href :label "\u00bb"))
|
||||
:weekdays (<> (map (lambda (wd) (~events-calendar-weekday :name wd))
|
||||
(or weekday-names (list))))
|
||||
:cells (<> (map (lambda (cell)
|
||||
(~events-calendar-cell
|
||||
:cell-cls (get cell "cell-cls")
|
||||
:day-short (when (get cell "day-str")
|
||||
(~events-calendar-day-short :day-str (get cell "day-str")))
|
||||
:day-num (when (get cell "day-href")
|
||||
(~events-calendar-day-num :pill-cls pill-cls
|
||||
:href (get cell "day-href") :num (get cell "day-num")))
|
||||
:badges (when (get cell "badges")
|
||||
(<> (map (lambda (b)
|
||||
(~events-calendar-entry-badge
|
||||
:bg-cls (get b "bg-cls") :name (get b "name")
|
||||
:state-label (get b "state-label")))
|
||||
(get cell "badges"))))))
|
||||
(or cells (list))))))
|
||||
|
||||
(defcomp ~events-calendar-description-display (&key description edit-url)
|
||||
(div :id "calendar-description"
|
||||
(if description
|
||||
|
||||
@@ -82,3 +82,42 @@
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" time-str))))
|
||||
|
||||
;; Day table from data — all row iteration in sx
|
||||
(defcomp ~events-day-table-from-data (&key list-container pre-action add-url tr-cls pill-cls rows)
|
||||
(~events-day-table
|
||||
:list-container list-container
|
||||
:rows (if (empty? (or rows (list)))
|
||||
(~events-day-empty-row)
|
||||
(<> (map (lambda (r)
|
||||
(~events-day-row
|
||||
:tr-cls tr-cls
|
||||
:name (~events-day-row-name
|
||||
:href (get r "href") :pill-cls pill-cls :name (get r "name"))
|
||||
:slot (if (get r "slot-name")
|
||||
(~events-day-row-slot
|
||||
:href (get r "slot-href") :pill-cls pill-cls
|
||||
:slot-name (get r "slot-name") :time-str (get r "slot-time"))
|
||||
(~events-day-row-time :start (get r "start") :end (get r "end")))
|
||||
:state (~events-day-row-state
|
||||
:state-id (get r "state-id")
|
||||
:badge (~entry-state-badge :state (get r "state")))
|
||||
:cost (~events-day-row-cost :cost-str (get r "cost-str"))
|
||||
:tickets (if (get r "has-tickets")
|
||||
(~events-day-row-tickets
|
||||
:price-str (get r "price-str") :count-str (get r "count-str"))
|
||||
(~events-day-row-no-tickets))
|
||||
:actions (~events-day-row-actions)))
|
||||
(or rows (list)))))
|
||||
:pre-action pre-action :add-url add-url))
|
||||
|
||||
;; Day entries nav OOB from data
|
||||
(defcomp ~events-day-entries-nav-oob-from-data (&key nav-btn entries)
|
||||
(if (empty? (or entries (list)))
|
||||
(~events-day-entries-nav-oob-empty)
|
||||
(~events-day-entries-nav-oob
|
||||
:items (<> (map (lambda (e)
|
||||
(~events-day-nav-entry
|
||||
:href (get e "href") :nav-btn nav-btn
|
||||
:name (get e "name") :time-str (get e "time-str")))
|
||||
entries)))))
|
||||
|
||||
@@ -1,5 +1,78 @@
|
||||
;; Events entry card components (all events / page summary)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; State badges — cond maps state string to class + label
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~entry-state-badge (&key state)
|
||||
(~badge
|
||||
:cls (cond
|
||||
((= state "confirmed") "bg-emerald-100 text-emerald-800")
|
||||
((= state "provisional") "bg-amber-100 text-amber-800")
|
||||
((= state "ordered") "bg-sky-100 text-sky-800")
|
||||
((= state "pending") "bg-stone-100 text-stone-700")
|
||||
((= state "declined") "bg-red-100 text-red-800")
|
||||
(true "bg-stone-100 text-stone-700"))
|
||||
:label (cond
|
||||
((= state "confirmed") "Confirmed")
|
||||
((= state "provisional") "Provisional")
|
||||
((= state "ordered") "Ordered")
|
||||
((= state "pending") "Pending")
|
||||
((= state "declined") "Declined")
|
||||
(true (or state "Unknown")))))
|
||||
|
||||
(defcomp ~entry-state-badge-lg (&key state)
|
||||
(span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium "
|
||||
(cond
|
||||
((= state "confirmed") "bg-emerald-100 text-emerald-800")
|
||||
((= state "provisional") "bg-amber-100 text-amber-800")
|
||||
((= state "ordered") "bg-sky-100 text-sky-800")
|
||||
((= state "pending") "bg-stone-100 text-stone-700")
|
||||
((= state "declined") "bg-red-100 text-red-800")
|
||||
(true "bg-stone-100 text-stone-700")))
|
||||
(cond
|
||||
((= state "confirmed") "Confirmed")
|
||||
((= state "provisional") "Provisional")
|
||||
((= state "ordered") "Ordered")
|
||||
((= state "pending") "Pending")
|
||||
((= state "declined") "Declined")
|
||||
(true (or state "Unknown")))))
|
||||
|
||||
(defcomp ~ticket-state-badge (&key state)
|
||||
(~badge
|
||||
:cls (cond
|
||||
((= state "confirmed") "bg-emerald-100 text-emerald-800")
|
||||
((= state "checked_in") "bg-blue-100 text-blue-800")
|
||||
((= state "reserved") "bg-amber-100 text-amber-800")
|
||||
((= state "cancelled") "bg-red-100 text-red-800")
|
||||
(true "bg-stone-100 text-stone-700"))
|
||||
:label (cond
|
||||
((= state "confirmed") "Confirmed")
|
||||
((= state "checked_in") "Checked in")
|
||||
((= state "reserved") "Reserved")
|
||||
((= state "cancelled") "Cancelled")
|
||||
(true (or state "Unknown")))))
|
||||
|
||||
(defcomp ~ticket-state-badge-lg (&key state)
|
||||
(span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium "
|
||||
(cond
|
||||
((= state "confirmed") "bg-emerald-100 text-emerald-800")
|
||||
((= state "checked_in") "bg-blue-100 text-blue-800")
|
||||
((= state "reserved") "bg-amber-100 text-amber-800")
|
||||
((= state "cancelled") "bg-red-100 text-red-800")
|
||||
(true "bg-stone-100 text-stone-700")))
|
||||
(cond
|
||||
((= state "confirmed") "Confirmed")
|
||||
((= state "checked_in") "Checked in")
|
||||
((= state "reserved") "Reserved")
|
||||
((= state "cancelled") "Cancelled")
|
||||
(true (or state "Unknown")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Entry card components
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(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)))
|
||||
@@ -63,3 +136,127 @@
|
||||
|
||||
(defcomp ~events-main-panel-body (&key toggle body)
|
||||
(<> toggle body (div :class "pb-8")))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Composition defcomps — receive data, compose entry card trees
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Ticket widget from data — replaces _ticket_widget_html Python composition
|
||||
(defcomp ~events-tw-widget-from-data (&key entry-id price qty ticket-url csrf)
|
||||
(~events-tw-widget :entry-id (str entry-id) :price price
|
||||
:inner (if (= (or qty 0) 0)
|
||||
(~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id)
|
||||
:csrf csrf :entry-id (str entry-id) :count-val "1"
|
||||
:btn (~events-tw-cart-plus))
|
||||
(<>
|
||||
(~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id)
|
||||
:csrf csrf :entry-id (str entry-id) :count-val (str (- qty 1))
|
||||
:btn (~events-tw-minus))
|
||||
(~events-tw-cart-icon :qty (str qty))
|
||||
(~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id)
|
||||
:csrf csrf :entry-id (str entry-id) :count-val (str (+ qty 1))
|
||||
:btn (~events-tw-plus))))))
|
||||
|
||||
;; Entry card (list view) from data
|
||||
(defcomp ~events-entry-card-from-data (&key entry-href name day-href
|
||||
page-badge-href page-badge-title cal-name
|
||||
date-str start-time end-time is-page-scoped
|
||||
cost has-ticket ticket-data)
|
||||
(~events-entry-card
|
||||
:title (if entry-href
|
||||
(~events-entry-title-linked :href entry-href :name name)
|
||||
(~events-entry-title-plain :name name))
|
||||
:badges (<>
|
||||
(when page-badge-title
|
||||
(~events-entry-page-badge :href page-badge-href :title page-badge-title))
|
||||
(when cal-name
|
||||
(~events-entry-cal-badge :name cal-name)))
|
||||
:time-parts (<>
|
||||
(when (and day-href (not is-page-scoped))
|
||||
(~events-entry-time-linked :href day-href :date-str date-str))
|
||||
(when (and (not day-href) (not is-page-scoped) date-str)
|
||||
(~events-entry-time-plain :date-str date-str))
|
||||
start-time
|
||||
(when end-time (str " \u2013 " end-time)))
|
||||
:cost (when cost (~events-entry-cost :cost cost))
|
||||
:widget (when has-ticket
|
||||
(~events-entry-widget-wrapper
|
||||
:widget (~events-tw-widget-from-data
|
||||
:entry-id (get ticket-data "entry-id")
|
||||
:price (get ticket-data "price")
|
||||
:qty (get ticket-data "qty")
|
||||
:ticket-url (get ticket-data "ticket-url")
|
||||
:csrf (get ticket-data "csrf"))))))
|
||||
|
||||
;; Entry card (tile view) from data
|
||||
(defcomp ~events-entry-card-tile-from-data (&key entry-href name day-href
|
||||
page-badge-href page-badge-title cal-name
|
||||
date-str time-str
|
||||
cost has-ticket ticket-data)
|
||||
(~events-entry-card-tile
|
||||
:title (if entry-href
|
||||
(~events-entry-title-tile-linked :href entry-href :name name)
|
||||
(~events-entry-title-tile-plain :name name))
|
||||
:badges (<>
|
||||
(when page-badge-title
|
||||
(~events-entry-page-badge :href page-badge-href :title page-badge-title))
|
||||
(when cal-name
|
||||
(~events-entry-cal-badge :name cal-name)))
|
||||
:time time-str
|
||||
:cost (when cost (~events-entry-cost :cost cost))
|
||||
:widget (when has-ticket
|
||||
(~events-entry-tile-widget-wrapper
|
||||
:widget (~events-tw-widget-from-data
|
||||
:entry-id (get ticket-data "entry-id")
|
||||
:price (get ticket-data "price")
|
||||
:qty (get ticket-data "qty")
|
||||
:ticket-url (get ticket-data "ticket-url")
|
||||
:csrf (get ticket-data "csrf"))))))
|
||||
|
||||
;; Entry cards list (with date separators + sentinel) from data
|
||||
(defcomp ~events-entry-cards-from-data (&key items view page has-more next-url)
|
||||
(<>
|
||||
(map (lambda (item)
|
||||
(if (get item "is-separator")
|
||||
(~events-date-separator :date-str (get item "date-str"))
|
||||
(if (= view "tile")
|
||||
(~events-entry-card-tile-from-data
|
||||
:entry-href (get item "entry-href") :name (get item "name")
|
||||
:day-href (get item "day-href")
|
||||
:page-badge-href (get item "page-badge-href")
|
||||
:page-badge-title (get item "page-badge-title")
|
||||
:cal-name (get item "cal-name")
|
||||
:date-str (get item "date-str") :time-str (get item "time-str")
|
||||
:cost (get item "cost") :has-ticket (get item "has-ticket")
|
||||
:ticket-data (get item "ticket-data"))
|
||||
(~events-entry-card-from-data
|
||||
:entry-href (get item "entry-href") :name (get item "name")
|
||||
:day-href (get item "day-href")
|
||||
:page-badge-href (get item "page-badge-href")
|
||||
:page-badge-title (get item "page-badge-title")
|
||||
:cal-name (get item "cal-name")
|
||||
:date-str (get item "date-str")
|
||||
:start-time (get item "start-time") :end-time (get item "end-time")
|
||||
:is-page-scoped (get item "is-page-scoped")
|
||||
:cost (get item "cost") :has-ticket (get item "has-ticket")
|
||||
:ticket-data (get item "ticket-data")))))
|
||||
(or items (list)))
|
||||
(when has-more
|
||||
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
|
||||
|
||||
;; Events main panel (toggle + cards grid) from data
|
||||
(defcomp ~events-main-panel-from-data (&key toggle items view page has-more next-url)
|
||||
(~events-main-panel-body
|
||||
:toggle toggle
|
||||
:body (if items
|
||||
(~events-grid
|
||||
:grid-cls (if (= view "tile")
|
||||
"max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||
"max-w-full px-3 py-3 space-y-3")
|
||||
:cards (~events-entry-cards-from-data
|
||||
:items items :view view :page page
|
||||
:has-more has-more :next-url next-url))
|
||||
(~empty-state :icon "fa fa-calendar-xmark"
|
||||
:message "No upcoming events"
|
||||
:cls "px-3 py-12 text-center text-stone-400"))))
|
||||
|
||||
@@ -318,6 +318,64 @@
|
||||
"+ Add slot"))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Composition defcomps — receive data, compose form trees
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Day checkboxes from data — replaces Python loop
|
||||
(defcomp ~events-day-checkboxes-from-data (&key days-data all-checked)
|
||||
(<>
|
||||
(~events-day-all-checkbox :checked (when all-checked "checked"))
|
||||
(map (lambda (d)
|
||||
(~events-day-checkbox
|
||||
:name (get d "name")
|
||||
:label (get d "label")
|
||||
:checked (when (get d "checked") "checked")))
|
||||
(or days-data (list)))))
|
||||
|
||||
;; Slot options from data — replaces _slot_options_html Python loop
|
||||
(defcomp ~events-slot-options-from-data (&key slots)
|
||||
(<> (map (lambda (s)
|
||||
(~events-slot-option
|
||||
:value (get s "value")
|
||||
:data-start (get s "data-start")
|
||||
:data-end (get s "data-end")
|
||||
:data-flexible (get s "data-flexible")
|
||||
:data-cost (get s "data-cost")
|
||||
:selected (get s "selected")
|
||||
:label (get s "label")))
|
||||
(or slots (list)))))
|
||||
|
||||
;; Slot picker from data — wraps picker + options
|
||||
(defcomp ~events-slot-picker-from-data (&key id slots)
|
||||
(if (empty? (or slots (list)))
|
||||
(~events-no-slots)
|
||||
(~events-slot-picker
|
||||
:id id
|
||||
:options (~events-slot-options-from-data :slots slots))))
|
||||
|
||||
;; Slot edit form from data
|
||||
(defcomp ~events-slot-edit-form-from-data (&key slot-id list-container put-url cancel-url csrf
|
||||
name-val cost-val start-val end-val desc-val
|
||||
days-data all-checked flexible-checked
|
||||
action-btn cancel-btn)
|
||||
(~events-slot-edit-form
|
||||
:slot-id slot-id :list-container list-container
|
||||
:put-url put-url :cancel-url cancel-url :csrf csrf
|
||||
:name-val name-val :cost-val cost-val :start-val start-val
|
||||
:end-val end-val :desc-val desc-val
|
||||
:days (~events-day-checkboxes-from-data :days-data days-data :all-checked all-checked)
|
||||
:flexible-checked flexible-checked
|
||||
:action-btn action-btn :cancel-btn cancel-btn))
|
||||
|
||||
;; Slot add form from data
|
||||
(defcomp ~events-slot-add-form-from-data (&key post-url csrf days-data action-btn cancel-btn cancel-url)
|
||||
(~events-slot-add-form
|
||||
:post-url post-url :csrf csrf
|
||||
:days (~events-day-checkboxes-from-data :days-data days-data)
|
||||
:action-btn action-btn :cancel-btn cancel-btn :cancel-url cancel-url))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Entry add form (_types/day/_add.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -68,3 +68,65 @@
|
||||
|
||||
(defcomp ~events-frag-bookings-list (&key items)
|
||||
(div :class "divide-y divide-stone-100" items))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; From-data defcomps — iteration in sx
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Container cards: list of widgets, each with entries
|
||||
(defcomp ~events-frag-container-cards-from-data (&key widgets)
|
||||
(<> (map (lambda (w)
|
||||
(if (get w "entries")
|
||||
(~events-frag-entries-widget
|
||||
:cards (<> (map (lambda (e)
|
||||
(~events-frag-entry-card
|
||||
:href (get e "href") :name (get e "name")
|
||||
:date-str (get e "date-str") :time-str (get e "time-str")))
|
||||
(get w "entries"))))
|
||||
""))
|
||||
(or widgets (list)))))
|
||||
|
||||
;; Ticket item from data — composes badge + optional spans
|
||||
(defcomp ~events-frag-ticket-item-from-data (&key href entry-name date-str calendar-name type-name state)
|
||||
(~events-frag-ticket-item
|
||||
:href href :entry-name entry-name :date-str date-str
|
||||
:calendar-name (when calendar-name (span "\u00b7 " calendar-name))
|
||||
:type-name (when type-name (span "\u00b7 " type-name))
|
||||
:badge (~status-pill :status state)))
|
||||
|
||||
;; Tickets panel from data — full panel with list iteration
|
||||
(defcomp ~events-frag-tickets-panel-from-data (&key tickets)
|
||||
(~events-frag-tickets-panel
|
||||
:items (if (empty? (or tickets (list)))
|
||||
(~empty-state :message "No tickets yet." :cls "text-sm text-stone-500")
|
||||
(~events-frag-tickets-list
|
||||
:items (<> (map (lambda (t)
|
||||
(~events-frag-ticket-item-from-data
|
||||
:href (get t "href") :entry-name (get t "entry-name")
|
||||
:date-str (get t "date-str") :calendar-name (get t "calendar-name")
|
||||
:type-name (get t "type-name") :state (get t "state")))
|
||||
tickets))))))
|
||||
|
||||
;; Booking item from data — composes badge + optional spans
|
||||
(defcomp ~events-frag-booking-item-from-data (&key name date-str end-time calendar-name cost-str state)
|
||||
(~events-frag-booking-item
|
||||
:name name
|
||||
:date-str (<> date-str (when end-time (span "\u2013 " end-time)))
|
||||
:calendar-name (when calendar-name (span "\u00b7 " calendar-name))
|
||||
:cost-str (when cost-str (span "\u00b7 \u00a3" cost-str))
|
||||
:badge (~status-pill :status state)))
|
||||
|
||||
;; Bookings panel from data — full panel with list iteration
|
||||
(defcomp ~events-frag-bookings-panel-from-data (&key bookings)
|
||||
(~events-frag-bookings-panel
|
||||
:items (if (empty? (or bookings (list)))
|
||||
(~empty-state :message "No bookings yet." :cls "text-sm text-stone-500")
|
||||
(~events-frag-bookings-list
|
||||
:items (<> (map (lambda (b)
|
||||
(~events-frag-booking-item-from-data
|
||||
:href (get b "href") :name (get b "name")
|
||||
:date-str (get b "date-str") :end-time (get b "end-time")
|
||||
:calendar-name (get b "calendar-name") :cost-str (get b "cost-str")
|
||||
:state (get b "state")))
|
||||
bookings))))))
|
||||
|
||||
@@ -226,6 +226,54 @@
|
||||
(~clear-oob-div :id "calendars-row")
|
||||
(~clear-oob-div :id "calendars-header-child")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; OOB clear helpers for renders.py — clear all deeper IDs except kept ones
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-clear-deeper-post ()
|
||||
"Clear all events IDs deeper than post level."
|
||||
(<>
|
||||
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child")
|
||||
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child")
|
||||
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child")
|
||||
(~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child")
|
||||
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child")
|
||||
(~clear-oob-div :id "calendar-row") (~clear-oob-div :id "calendar-header-child")
|
||||
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child")
|
||||
(~clear-oob-div :id "post-admin-row") (~clear-oob-div :id "post-admin-header-child")))
|
||||
|
||||
(defcomp ~events-clear-deeper-post-admin ()
|
||||
"Clear all events IDs deeper than post-admin level."
|
||||
(<>
|
||||
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child")
|
||||
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child")
|
||||
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child")
|
||||
(~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child")
|
||||
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child")
|
||||
(~clear-oob-div :id "calendar-row") (~clear-oob-div :id "calendar-header-child")
|
||||
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child")))
|
||||
|
||||
(defcomp ~events-clear-deeper-calendar ()
|
||||
"Clear all events IDs deeper than calendar level."
|
||||
(<>
|
||||
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child")
|
||||
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child")
|
||||
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child")
|
||||
(~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child")
|
||||
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child")
|
||||
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child")
|
||||
(~clear-oob-div :id "post-admin-row") (~clear-oob-div :id "post-admin-header-child")))
|
||||
|
||||
(defcomp ~events-clear-deeper-day ()
|
||||
"Clear all events IDs deeper than day level."
|
||||
(<>
|
||||
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child")
|
||||
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child")
|
||||
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child")
|
||||
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child")
|
||||
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child")
|
||||
(~clear-oob-div :id "post-admin-row") (~clear-oob-div :id "post-admin-header-child")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Calendar admin layout: root + post + child(post-admin + cal + cal-admin)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -73,6 +73,50 @@
|
||||
:sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
|
||||
"+ Add slot"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Composition defcomps — receive data, compose slot/table trees
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Days pills from data — replaces Python loop
|
||||
(defcomp ~events-days-pills-from-data (&key days)
|
||||
(if (empty? (or days (list)))
|
||||
(~events-slot-no-days)
|
||||
(~events-slot-days-pills
|
||||
:days-inner (<> (map (lambda (d) (~events-slot-day-pill :day d)) days)))))
|
||||
|
||||
;; Slot panel from data
|
||||
(defcomp ~events-slot-panel-from-data (&key slot-id list-container days
|
||||
flexible time-str cost-str
|
||||
pre-action edit-url description oob)
|
||||
(<>
|
||||
(~events-slot-panel
|
||||
:slot-id slot-id :list-container list-container
|
||||
:days (~events-days-pills-from-data :days days)
|
||||
:flexible flexible :time-str time-str :cost-str cost-str
|
||||
:pre-action pre-action :edit-url edit-url)
|
||||
(when oob
|
||||
(~events-slot-description-oob :description (or description "")))))
|
||||
|
||||
;; Slots table from data
|
||||
(defcomp ~events-slots-table-from-data (&key list-container slots pre-action add-url
|
||||
tr-cls pill-cls action-btn hx-select csrf-hdr)
|
||||
(~events-slots-table
|
||||
:list-container list-container
|
||||
:rows (if (empty? (or slots (list)))
|
||||
(~events-slots-empty-row)
|
||||
(<> (map (lambda (s)
|
||||
(~events-slots-row
|
||||
:tr-cls tr-cls :slot-href (get s "slot-href")
|
||||
:pill-cls pill-cls :hx-select hx-select
|
||||
:slot-name (get s "slot-name") :description (get s "description")
|
||||
:flexible (get s "flexible")
|
||||
:days (~events-days-pills-from-data :days (get s "days"))
|
||||
:time-str (get s "time-str")
|
||||
:cost-str (get s "cost-str") :action-btn action-btn
|
||||
:del-url (get s "del-url") :csrf-hdr csrf-hdr))
|
||||
(or slots (list)))))
|
||||
:pre-action pre-action :add-url add-url))
|
||||
|
||||
(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)
|
||||
@@ -158,102 +202,138 @@
|
||||
(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"))))
|
||||
|
||||
;; Data-driven buy form — Python passes pre-resolved data, .sx does layout + iteration
|
||||
(defcomp ~events-buy-form (&key entry-id info-sold info-remaining info-basket
|
||||
ticket-types user-ticket-counts-by-type
|
||||
user-ticket-count price-str adjust-url csrf state
|
||||
my-tickets-href)
|
||||
(if (!= state "confirmed")
|
||||
(~events-buy-not-confirmed :entry-id (str entry-id))
|
||||
(let ((eid-s (str entry-id))
|
||||
(target (str "#ticket-buy-" entry-id)))
|
||||
(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 bar
|
||||
(when (or info-sold info-remaining info-basket)
|
||||
(div :class "flex items-center gap-3 mb-3 text-xs text-stone-500"
|
||||
(when info-sold (span (str info-sold " sold")))
|
||||
(when info-remaining (span (str info-remaining " remaining")))
|
||||
(when info-basket
|
||||
(span :class "text-emerald-600 font-medium"
|
||||
(i :class "fa fa-shopping-cart text-[0.6rem]" :aria-hidden "true")
|
||||
(str " " info-basket " in basket")))))
|
||||
;; Body — multi-type or default
|
||||
(if (and ticket-types (not (empty? ticket-types)))
|
||||
(div :class "space-y-2"
|
||||
(map (fn (tt)
|
||||
(let ((tt-count (if user-ticket-counts-by-type
|
||||
(get user-ticket-counts-by-type (str (get tt "id")) 0)
|
||||
0))
|
||||
(tt-id (get tt "id")))
|
||||
(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" (get tt "name"))
|
||||
(div :class "text-xs text-stone-500" (get tt "cost_str")))
|
||||
(~events-adjust-inline :csrf csrf :adjust-url adjust-url :target target
|
||||
:entry-id eid-s :count tt-count :ticket-type-id tt-id
|
||||
:my-tickets-href my-tickets-href))))
|
||||
ticket-types))
|
||||
(<> (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")))
|
||||
(~events-adjust-inline :csrf csrf :adjust-url adjust-url :target target
|
||||
:entry-id eid-s :count (if user-ticket-count user-ticket-count 0)
|
||||
:ticket-type-id nil :my-tickets-href my-tickets-href)))))))
|
||||
|
||||
;; Inline +/- controls (used by both default and per-type)
|
||||
(defcomp ~events-adjust-inline (&key csrf adjust-url target entry-id count ticket-type-id my-tickets-href)
|
||||
(if (= count 0)
|
||||
(form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" :class "flex items-center"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
(when ticket-type-id (input :type "hidden" :name "ticket_type_id" :value (str ticket-type-id)))
|
||||
(input :type "hidden" :name "count" :value "1")
|
||||
(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")))
|
||||
(div :class "flex items-center gap-2"
|
||||
(form :sx-post adjust-url :sx-target target :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
(when ticket-type-id (input :type "hidden" :name "ticket_type_id" :value (str ticket-type-id)))
|
||||
(input :type "hidden" :name "count" :value (str (- count 1)))
|
||||
(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"
|
||||
"-"))
|
||||
(a :class "relative inline-flex items-center justify-center text-emerald-700" :href my-tickets-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" (str count)))))
|
||||
(form :sx-post adjust-url :sx-target target :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
(when ticket-type-id (input :type "hidden" :name "ticket_type_id" :value (str ticket-type-id)))
|
||||
(input :type "hidden" :name "count" :value (str (+ count 1)))
|
||||
(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-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-result (&key entry-id tickets remaining my-tickets-href)
|
||||
(let ((count (len tickets))
|
||||
(suffix (if (= count 1) "" "s")))
|
||||
(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" (str count " ticket" suffix " reserved")))
|
||||
(div :class "space-y-2 mb-4"
|
||||
(map (fn (t)
|
||||
(a :href (get t "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" (get t "code_short")))
|
||||
(span :class "text-xs text-emerald-600 font-medium" "View ticket")))
|
||||
tickets))
|
||||
(when (not (nil? remaining))
|
||||
(let ((r-suffix (if (= remaining 1) "" "s")))
|
||||
(p :class "text-xs text-stone-500" (str remaining " ticket" r-suffix " 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-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")))
|
||||
;; Single response wrappers for POST routes (include OOB cart icon)
|
||||
(defcomp ~events-buy-response (&key entry-id tickets remaining my-tickets-href
|
||||
cart-count blog-href cart-href logo)
|
||||
(<>
|
||||
(~events-cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo)
|
||||
(~events-buy-result :entry-id entry-id :tickets tickets :remaining remaining
|
||||
:my-tickets-href my-tickets-href)))
|
||||
|
||||
(defcomp ~events-buy-info-bar (&key items)
|
||||
(div :class "flex items-center gap-3 mb-3 text-xs text-stone-500" items))
|
||||
(defcomp ~events-adjust-response (&key cart-count blog-href cart-href logo
|
||||
entry-id info-sold info-remaining info-basket
|
||||
ticket-types user-ticket-counts-by-type
|
||||
user-ticket-count price-str adjust-url csrf state
|
||||
my-tickets-href)
|
||||
(<>
|
||||
(~events-cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo)
|
||||
(~events-buy-form :entry-id entry-id :info-sold info-sold :info-remaining info-remaining
|
||||
:info-basket info-basket :ticket-types ticket-types
|
||||
:user-ticket-counts-by-type user-ticket-counts-by-type
|
||||
:user-ticket-count user-ticket-count :price-str price-str
|
||||
:adjust-url adjust-url :csrf csrf :state state
|
||||
:my-tickets-href my-tickets-href)))
|
||||
|
||||
(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))
|
||||
;; Unified OOB cart icon — picks logo or badge based on count
|
||||
(defcomp ~events-cart-icon (&key cart-count blog-href cart-href logo)
|
||||
(if (= cart-count 0)
|
||||
(~events-cart-icon-logo :blog-href blog-href :logo logo)
|
||||
(~events-cart-icon-badge :cart-href cart-href :count (str cart-count))))
|
||||
|
||||
(defcomp ~events-cart-icon-logo (&key blog-href logo)
|
||||
(div :id "cart-mini" :sx-swap-oob "true"
|
||||
@@ -384,3 +464,169 @@
|
||||
(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))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Composition defcomps — nav OOB, posts panel from data
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Post image helper from data
|
||||
(defcomp ~events-post-img-from-data (&key src alt)
|
||||
(if src
|
||||
(~events-post-img :src src :alt alt)
|
||||
(~events-post-img-placeholder)))
|
||||
|
||||
;; Entry posts nav OOB from data
|
||||
(defcomp ~events-entry-posts-nav-oob-from-data (&key nav-btn posts)
|
||||
(if (empty? (or posts (list)))
|
||||
(~events-entry-posts-nav-oob-empty)
|
||||
(~events-entry-posts-nav-oob
|
||||
:items (<> (map (lambda (p)
|
||||
(~events-entry-nav-post
|
||||
:href (get p "href") :nav-btn nav-btn
|
||||
:img (~events-post-img-from-data :src (get p "img") :alt (get p "title"))
|
||||
:title (get p "title")))
|
||||
posts)))))
|
||||
|
||||
;; Entry posts nav (non-OOB) from data — for desktop nav embedding
|
||||
(defcomp ~events-entry-posts-nav-inner-from-data (&key posts)
|
||||
(when (not (empty? (or posts (list))))
|
||||
(~events-entry-posts-nav-oob
|
||||
:items (<> (map (lambda (p)
|
||||
(~events-entry-nav-post-link
|
||||
:href (get p "href")
|
||||
:img (~events-post-img-from-data :src (get p "img") :alt (get p "title"))
|
||||
:title (get p "title")))
|
||||
posts)))))
|
||||
|
||||
;; Post nav entries+calendars OOB from data
|
||||
(defcomp ~events-post-nav-wrapper-from-data (&key nav-btn entries calendars hyperscript)
|
||||
(if (and (empty? (or entries (list))) (empty? (or calendars (list))))
|
||||
(~events-post-nav-oob-empty)
|
||||
(~events-post-nav-wrapper
|
||||
:items (<>
|
||||
(map (lambda (e)
|
||||
(~events-post-nav-entry
|
||||
:href (get e "href") :nav-btn nav-btn
|
||||
:name (get e "name") :time-str (get e "time-str")))
|
||||
(or entries (list)))
|
||||
(map (lambda (c)
|
||||
(~events-post-nav-calendar
|
||||
:href (get c "href") :nav-btn nav-btn :name (get c "name")))
|
||||
(or calendars (list))))
|
||||
:hyperscript hyperscript)))
|
||||
|
||||
;; Entry posts panel from data
|
||||
(defcomp ~events-entry-posts-panel-from-data (&key entry-id posts search-url)
|
||||
(~events-entry-posts-panel
|
||||
:posts (if (empty? (or posts (list)))
|
||||
(~events-entry-posts-none)
|
||||
(~events-entry-posts-list
|
||||
:items (<> (map (lambda (p)
|
||||
(~events-entry-post-item
|
||||
:img (~events-post-img-from-data :src (get p "img") :alt (get p "title"))
|
||||
:title (get p "title")
|
||||
:del-url (get p "del-url") :entry-id entry-id
|
||||
:csrf-hdr (get p "csrf-hdr")))
|
||||
posts))))
|
||||
:search-url search-url :entry-id entry-id))
|
||||
|
||||
;; CRUD list/panel from data — shared by calendars + markets
|
||||
(defcomp ~events-crud-list-from-data (&key items empty-msg list-id)
|
||||
(if (empty? (or items (list)))
|
||||
(~empty-state :message empty-msg :cls "text-gray-500 mt-4")
|
||||
(<> (map (lambda (item)
|
||||
(~crud-item
|
||||
:href (get item "href") :name (get item "name") :slug (get item "slug")
|
||||
:del-url (get item "del-url") :csrf-hdr (get item "csrf-hdr")
|
||||
:list-id list-id
|
||||
:confirm-title (get item "confirm-title")
|
||||
:confirm-text (get item "confirm-text")))
|
||||
items))))
|
||||
|
||||
(defcomp ~events-crud-panel-from-data (&key can-create create-url csrf errors-id list-id
|
||||
placeholder btn-label items empty-msg)
|
||||
(~crud-panel
|
||||
:form (when can-create
|
||||
(~crud-create-form
|
||||
:create-url create-url :csrf csrf :errors-id errors-id
|
||||
:list-id list-id :placeholder placeholder :btn-label btn-label))
|
||||
:list (~events-crud-list-from-data :items items :empty-msg empty-msg :list-id list-id)
|
||||
:list-id list-id))
|
||||
|
||||
;; Post nav admin cog
|
||||
(defcomp ~events-post-nav-admin-cog (&key href aclass)
|
||||
(div :class "relative nav-group"
|
||||
(a :href href :class aclass
|
||||
(i :class "fa fa-cog" :aria-hidden "true"))))
|
||||
|
||||
;; Post nav from data — calendar links + container nav + admin
|
||||
(defcomp ~events-post-nav-from-data (&key calendars container-nav select-colours
|
||||
has-admin admin-href aclass)
|
||||
(<>
|
||||
(map (lambda (c)
|
||||
(~nav-link :href (get c "href") :icon "fa fa-calendar"
|
||||
:label (get c "name") :select-colours select-colours
|
||||
:is-selected (get c "is-selected")))
|
||||
(or calendars (list)))
|
||||
(when container-nav container-nav)
|
||||
(when has-admin
|
||||
(~events-post-nav-admin-cog :href admin-href :aclass aclass))))
|
||||
|
||||
;; Calendar nav from data — slots + admin link
|
||||
(defcomp ~events-calendar-nav-from-data (&key slots-href admin-href select-colours is-admin)
|
||||
(<>
|
||||
(~nav-link :href slots-href :icon "fa fa-clock"
|
||||
:label "Slots" :select-colours select-colours)
|
||||
(when is-admin
|
||||
(~nav-link :href admin-href :icon "fa fa-cog"
|
||||
:select-colours select-colours))))
|
||||
|
||||
;; Calendar admin nav from data
|
||||
(defcomp ~events-calendar-admin-nav-from-data (&key links select-colours)
|
||||
(<> (map (lambda (l)
|
||||
(~nav-link :href (get l "href") :label (get l "label")
|
||||
:select-colours select-colours))
|
||||
(or links (list)))))
|
||||
|
||||
;; Day nav from data — confirmed entries + admin link
|
||||
(defcomp ~events-day-nav-from-data (&key entries is-admin admin-href)
|
||||
(<>
|
||||
(when (not (empty? (or entries (list))))
|
||||
(~events-day-entries-nav
|
||||
:inner (<> (map (lambda (e)
|
||||
(~events-day-entry-link
|
||||
:href (get e "href") :name (get e "name") :time-str (get e "time-str")))
|
||||
entries))))
|
||||
(when is-admin
|
||||
(~nav-link :href admin-href :icon "fa fa-cog"))))
|
||||
|
||||
;; Post search results from data
|
||||
(defcomp ~events-post-search-results-from-data (&key items page next-url has-more)
|
||||
(<>
|
||||
(map (lambda (item)
|
||||
(~events-post-search-item
|
||||
:post-url (get item "post-url") :entry-id (get item "entry-id")
|
||||
:csrf (get item "csrf") :post-id (get item "post-id")
|
||||
:img (~events-post-img-from-data :src (get item "img") :alt (get item "title"))
|
||||
:title (get item "title")))
|
||||
(or items (list)))
|
||||
(cond
|
||||
(has-more (~events-post-search-sentinel :page page :next-url next-url))
|
||||
((not (empty? (or items (list)))) (~events-post-search-end))
|
||||
(true ""))))
|
||||
|
||||
;; Entry options from data — state-driven button composition
|
||||
(defcomp ~events-entry-options-from-data (&key entry-id state buttons)
|
||||
(~events-entry-options
|
||||
:entry-id entry-id
|
||||
:buttons (<> (map (lambda (b)
|
||||
(~events-entry-option-button
|
||||
:url (get b "url") :target (str "#calendar_entry_options_" entry-id)
|
||||
:csrf (get b "csrf") :btn-type (get b "btn-type")
|
||||
:action-btn (get b "action-btn")
|
||||
:confirm-title (get b "confirm-title")
|
||||
:confirm-text (get b "confirm-text")
|
||||
:label (get b "label")
|
||||
:is-btn (get b "is-btn")))
|
||||
(or buttons (list))))))
|
||||
|
||||
@@ -204,3 +204,152 @@
|
||||
(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 ~events-tickets-panel-from-data (&key list-container tickets)
|
||||
(~events-tickets-panel
|
||||
:list-container list-container
|
||||
:has-tickets (not (empty? (or tickets (list))))
|
||||
:cards (<> (map (lambda (t)
|
||||
(~events-ticket-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 (~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 ~events-ticket-detail-from-data (&key list-container back-href header-bg entry-name
|
||||
state type-name code time-date time-range
|
||||
cal-name type-desc checkin-str qr-script)
|
||||
(~events-ticket-detail
|
||||
:list-container list-container :back-href back-href
|
||||
:header-bg header-bg :entry-name entry-name
|
||||
:badge (~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 ~events-ticket-admin-row-from-data (&key code code-short entry-name date-str
|
||||
type-name state checkin-url csrf
|
||||
checked-in-time)
|
||||
(~events-ticket-admin-row
|
||||
:code code :code-short code-short
|
||||
:entry-name entry-name
|
||||
:date (when date-str (~events-ticket-admin-date :date-str date-str))
|
||||
:type-name type-name
|
||||
:badge (~ticket-state-badge :state state)
|
||||
:action (cond
|
||||
((or (= state "confirmed") (= state "reserved"))
|
||||
(~events-ticket-admin-checkin-form
|
||||
:checkin-url checkin-url :code code :csrf csrf))
|
||||
((= state "checked_in")
|
||||
(~events-ticket-admin-checked-in :time-str (or checked-in-time "")))
|
||||
(true nil))))
|
||||
|
||||
;; Ticket admin panel from data
|
||||
(defcomp ~events-ticket-admin-panel-from-data (&key list-container lookup-url tickets
|
||||
total confirmed checked-in reserved)
|
||||
(~events-ticket-admin-panel
|
||||
:list-container list-container
|
||||
:stats (<>
|
||||
(~events-ticket-admin-stat :border "border-stone-200" :bg ""
|
||||
:text-cls "text-stone-900" :label-cls "text-stone-500"
|
||||
:value (str (or total 0)) :label "Total")
|
||||
(~events-ticket-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")
|
||||
(~events-ticket-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")
|
||||
(~events-ticket-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)
|
||||
(~events-ticket-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 ~events-entry-tickets-admin-from-data (&key entry-name count-label tickets csrf)
|
||||
(~events-entry-tickets-admin-panel
|
||||
:entry-name entry-name :count-label count-label
|
||||
:body (if (empty? (or tickets (list)))
|
||||
(~events-entry-tickets-admin-empty)
|
||||
(~events-entry-tickets-admin-table
|
||||
:rows (<> (map (lambda (t)
|
||||
(~events-entry-tickets-admin-row
|
||||
:code (get t "code") :code-short (get t "code-short")
|
||||
:type-name (get t "type-name")
|
||||
:badge (~ticket-state-badge :state (get t "state"))
|
||||
:action (cond
|
||||
((or (= (get t "state") "confirmed") (= (get t "state") "reserved"))
|
||||
(~events-entry-tickets-admin-checkin
|
||||
:checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf))
|
||||
((= (get t "state") "checked_in")
|
||||
(~events-ticket-admin-checked-in :time-str (or (get t "checked-in-time") "")))
|
||||
(true nil))))
|
||||
(or tickets (list))))))))
|
||||
|
||||
;; Checkin success row from data
|
||||
(defcomp ~events-checkin-success-row-from-data (&key code code-short entry-name date-str type-name time-str)
|
||||
(~events-checkin-success-row
|
||||
:code code :code-short code-short
|
||||
:entry-name entry-name
|
||||
:date (when date-str (~events-ticket-admin-date :date-str date-str))
|
||||
:type-name type-name
|
||||
:badge (~ticket-state-badge :state "checked_in")
|
||||
:time-str time-str))
|
||||
|
||||
;; Ticket types table from data
|
||||
(defcomp ~events-ticket-types-table-from-data (&key list-container ticket-types action-btn add-url
|
||||
tr-cls pill-cls hx-select csrf-hdr)
|
||||
(~events-ticket-types-table
|
||||
:list-container list-container
|
||||
:rows (if (empty? (or ticket-types (list)))
|
||||
(~events-ticket-types-empty-row)
|
||||
(<> (map (lambda (tt)
|
||||
(~events-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 ~events-lookup-result-from-data (&key entry-name type-name date-str cal-name
|
||||
state code checked-in-str
|
||||
checkin-url csrf)
|
||||
(~events-lookup-card
|
||||
:info (<>
|
||||
(~events-lookup-info :entry-name entry-name)
|
||||
(when type-name (~events-lookup-type :type-name type-name))
|
||||
(when date-str (~events-lookup-date :date-str date-str))
|
||||
(when cal-name (~events-lookup-cal :cal-name cal-name))
|
||||
(~events-lookup-status
|
||||
:badge (~ticket-state-badge :state state) :code code)
|
||||
(when checked-in-str
|
||||
(~events-lookup-checkin-time :date-str checked-in-str)))
|
||||
:code code
|
||||
:action (cond
|
||||
((or (= state "confirmed") (= state "reserved"))
|
||||
(~events-lookup-checkin-btn :checkin-url checkin-url :code code :csrf csrf))
|
||||
((= state "checked_in") (~events-lookup-checked-in))
|
||||
((= state "cancelled") (~events-lookup-cancelled))
|
||||
(true nil))))
|
||||
|
||||
@@ -8,8 +8,8 @@ from shared.sx.helpers import (
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
from .utils import (
|
||||
_clear_deeper_oob, _ensure_container_nav,
|
||||
_entry_state_badge_html, _list_container,
|
||||
_ensure_container_nav,
|
||||
_list_container,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,45 +27,41 @@ def _post_nav_sx(ctx: dict) -> str:
|
||||
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
|
||||
from quart import url_for, g
|
||||
|
||||
calendars = ctx.get("calendars") or []
|
||||
calendars_orm = ctx.get("calendars") or []
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
current_cal_slug = getattr(g, "calendar_slug", None)
|
||||
|
||||
parts = []
|
||||
for cal in calendars:
|
||||
calendars_data = []
|
||||
for cal in calendars_orm:
|
||||
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
|
||||
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
|
||||
href = url_for("calendar.get", calendar_slug=cal_slug)
|
||||
is_sel = (cal_slug == current_cal_slug)
|
||||
parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar",
|
||||
label=cal_name, select_colours=select_colours,
|
||||
is_selected=is_sel))
|
||||
# Container nav fragments (markets, etc.)
|
||||
container_nav = ctx.get("container_nav", "")
|
||||
if container_nav:
|
||||
parts.append(container_nav)
|
||||
calendars_data.append({
|
||||
"href": href, "name": cal_name,
|
||||
"is-selected": True if cal_slug == current_cal_slug else None,
|
||||
})
|
||||
|
||||
container_nav = ctx.get("container_nav", "") or None
|
||||
|
||||
# Admin cog → blog admin for this post (cross-domain, no HTMX)
|
||||
rights = ctx.get("rights") or {}
|
||||
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
||||
admin_href = None
|
||||
aclass = None
|
||||
if has_admin:
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
styles = ctx.get("styles") or {}
|
||||
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
||||
aclass = f"{nav_btn} {select_colours}".strip() or (
|
||||
"justify-center cursor-pointer flex flex-row items-center gap-2 "
|
||||
"rounded bg-stone-200 text-black p-3"
|
||||
)
|
||||
parts.append(
|
||||
f'<div class="relative nav-group">'
|
||||
f'<a href="{admin_href}" class="{aclass}">'
|
||||
f'<i class="fa fa-cog" aria-hidden="true"></i></a></div>'
|
||||
)
|
||||
|
||||
return "".join(parts)
|
||||
return sx_call("events-post-nav-from-data",
|
||||
calendars=calendars_data or None, container_nav=container_nav,
|
||||
select_colours=select_colours,
|
||||
has_admin=has_admin or None, admin_href=admin_href, aclass=aclass)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -119,15 +115,13 @@ def _calendar_nav_sx(ctx: dict) -> str:
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
|
||||
parts = []
|
||||
slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug)
|
||||
parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
|
||||
label="Slots", select_colours=select_colours))
|
||||
if is_admin:
|
||||
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug)
|
||||
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
|
||||
select_colours=select_colours))
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug) if is_admin else None
|
||||
|
||||
return sx_call("events-calendar-nav-from-data",
|
||||
slots_href=slots_href, admin_href=admin_href,
|
||||
select_colours=select_colours,
|
||||
is_admin=is_admin or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -174,27 +168,21 @@ def _day_nav_sx(ctx: dict) -> str:
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
|
||||
parts = []
|
||||
# Confirmed entries nav (scrolling menu)
|
||||
if confirmed_entries:
|
||||
entry_links = []
|
||||
for entry in confirmed_entries:
|
||||
href = url_for(
|
||||
"defpage_entry_detail",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year,
|
||||
month=day_date.month,
|
||||
day=day_date.day,
|
||||
entry_id=entry.id,
|
||||
)
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
entry_links.append(sx_call("events-day-entry-link",
|
||||
href=href, name=entry.name,
|
||||
time_str=f"{start}{end}"))
|
||||
inner = "".join(entry_links)
|
||||
parts.append(sx_call("events-day-entries-nav", inner=SxExpr(inner)))
|
||||
entries_data = []
|
||||
for entry in confirmed_entries:
|
||||
href = url_for(
|
||||
"defpage_entry_detail",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year,
|
||||
month=day_date.month,
|
||||
day=day_date.day,
|
||||
entry_id=entry.id,
|
||||
)
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
entries_data.append({"href": href, "name": entry.name, "time-str": f"{start}{end}"})
|
||||
|
||||
admin_href = None
|
||||
if is_admin and day_date:
|
||||
admin_href = url_for(
|
||||
"defpage_day_admin",
|
||||
@@ -203,8 +191,10 @@ def _day_nav_sx(ctx: dict) -> str:
|
||||
month=day_date.month,
|
||||
day=day_date.day,
|
||||
)
|
||||
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog"))
|
||||
return "".join(parts)
|
||||
|
||||
return sx_call("events-day-nav-from-data",
|
||||
entries=entries_data or None,
|
||||
is_admin=is_admin or None, admin_href=admin_href)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -245,17 +235,16 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
cal_slug = getattr(calendar, "slug", "") if calendar else ""
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
|
||||
nav_parts = []
|
||||
links_data = []
|
||||
if cal_slug:
|
||||
for endpoint, label in [
|
||||
("defpage_slots_listing", "slots"),
|
||||
("calendar.admin.calendar_description_edit", "description"),
|
||||
]:
|
||||
href = url_for(endpoint, calendar_slug=cal_slug)
|
||||
nav_parts.append(sx_call("nav-link", href=href, label=label,
|
||||
select_colours=select_colours))
|
||||
links_data.append({"href": url_for(endpoint, calendar_slug=cal_slug), "label": label})
|
||||
|
||||
nav_html = "".join(nav_parts)
|
||||
nav_html = sx_call("events-calendar-admin-nav-from-data",
|
||||
links=links_data or None, select_colours=select_colours) if links_data else ""
|
||||
return sx_call("menu-row-sx", id="calendar-admin-row", level=4,
|
||||
link_label="admin", icon="fa fa-cog",
|
||||
nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob)
|
||||
@@ -282,55 +271,36 @@ def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
def _calendars_main_panel_sx(ctx: dict) -> str:
|
||||
"""Render the calendars list + create form panel."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
has_access = ctx.get("has_access")
|
||||
can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
|
||||
calendars = ctx.get("calendars") or []
|
||||
|
||||
form_html = ""
|
||||
if can_create:
|
||||
create_url = url_for("calendars.create_calendar")
|
||||
form_html = sx_call("crud-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
errors_id="cal-create-errors", list_id="calendars-list",
|
||||
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar")
|
||||
|
||||
list_html = _calendars_list_sx(ctx, calendars)
|
||||
return sx_call("crud-panel",
|
||||
form=SxExpr(form_html), list=SxExpr(list_html),
|
||||
list_id="calendars-list")
|
||||
|
||||
|
||||
def _calendars_list_sx(ctx: dict, calendars: list) -> str:
|
||||
"""Render the calendars list items."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
prefix = route_prefix()
|
||||
|
||||
if not calendars:
|
||||
return sx_call("empty-state", message="No calendars yet. Create one above.",
|
||||
cls="text-gray-500 mt-4")
|
||||
|
||||
parts = []
|
||||
calendars = ctx.get("calendars") or []
|
||||
items_data = []
|
||||
for cal in calendars:
|
||||
cal_slug = getattr(cal, "slug", "")
|
||||
cal_name = getattr(cal, "name", "")
|
||||
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
|
||||
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
parts.append(sx_call("crud-item",
|
||||
href=href, name=cal_name, slug=cal_slug,
|
||||
del_url=del_url, csrf_hdr=csrf_hdr,
|
||||
list_id="calendars-list",
|
||||
confirm_title="Delete calendar?",
|
||||
confirm_text="Entries will be hidden (soft delete)"))
|
||||
return "".join(parts)
|
||||
items_data.append({
|
||||
"href": prefix + url_for("calendar.get", calendar_slug=cal_slug),
|
||||
"name": cal_name, "slug": cal_slug,
|
||||
"del-url": url_for("calendar.delete", calendar_slug=cal_slug),
|
||||
"csrf-hdr": f'{{"X-CSRFToken":"{csrf}"}}',
|
||||
"confirm-title": "Delete calendar?",
|
||||
"confirm-text": "Entries will be hidden (soft delete)",
|
||||
})
|
||||
|
||||
return sx_call("events-crud-panel-from-data",
|
||||
can_create=can_create or None,
|
||||
create_url=url_for("calendars.create_calendar") if can_create else None,
|
||||
csrf=csrf, errors_id="cal-create-errors", list_id="calendars-list",
|
||||
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar",
|
||||
items=items_data or None,
|
||||
empty_msg="No calendars yet. Create one above.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -338,7 +308,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
"""Render the calendar month grid."""
|
||||
"""Render the calendar month grid via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from quart import session as qsession
|
||||
|
||||
@@ -346,7 +316,6 @@ def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
styles = ctx.get("styles") or {}
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
|
||||
@@ -368,35 +337,8 @@ def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
def nav_link(y, m):
|
||||
return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m)
|
||||
|
||||
# Month navigation arrows
|
||||
nav_arrows = []
|
||||
for label, yr, mn in [
|
||||
("\u00ab", prev_year, month),
|
||||
("\u2039", prev_month_year, prev_month),
|
||||
]:
|
||||
href = nav_link(yr, mn)
|
||||
nav_arrows.append(sx_call("events-calendar-nav-arrow",
|
||||
pill_cls=pill_cls, href=href, label=label))
|
||||
|
||||
nav_arrows.append(sx_call("events-calendar-month-label",
|
||||
month_name=month_name, year=str(year)))
|
||||
|
||||
for label, yr, mn in [
|
||||
("\u203a", next_month_year, next_month),
|
||||
("\u00bb", next_year, month),
|
||||
]:
|
||||
href = nav_link(yr, mn)
|
||||
nav_arrows.append(sx_call("events-calendar-nav-arrow",
|
||||
pill_cls=pill_cls, href=href, label=label))
|
||||
|
||||
# Weekday headers
|
||||
wd_parts = []
|
||||
for wd in weekday_names:
|
||||
wd_parts.append(sx_call("events-calendar-weekday", name=wd))
|
||||
wd_html = "".join(wd_parts)
|
||||
|
||||
# Day cells
|
||||
cells = []
|
||||
# Day cells data
|
||||
cells_data = []
|
||||
for week in weeks:
|
||||
for day_cell in week:
|
||||
if isinstance(day_cell, dict):
|
||||
@@ -414,24 +356,18 @@ def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
if is_today:
|
||||
cell_cls += " ring-2 ring-blue-500 z-10 relative"
|
||||
|
||||
# Day number link
|
||||
day_num_html = ""
|
||||
day_short_html = ""
|
||||
cell = {"cell-cls": cell_cls}
|
||||
if day_date:
|
||||
day_href = url_for(
|
||||
cell["day-str"] = day_date.strftime("%a")
|
||||
cell["day-href"] = url_for(
|
||||
"calendar.day.show_day",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
)
|
||||
day_short_html = sx_call("events-calendar-day-short",
|
||||
day_str=day_date.strftime("%a"))
|
||||
day_num_html = sx_call("events-calendar-day-num",
|
||||
pill_cls=pill_cls, href=day_href,
|
||||
num=str(day_date.day))
|
||||
cell["day-num"] = str(day_date.day)
|
||||
|
||||
# Entry badges for this day
|
||||
entry_badges = []
|
||||
if day_date:
|
||||
# Entry badges for this day
|
||||
badges = []
|
||||
for e in month_entries:
|
||||
if e.start_at and e.start_at.date() == day_date:
|
||||
is_mine = (
|
||||
@@ -442,23 +378,23 @@ def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800"
|
||||
else:
|
||||
bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700"
|
||||
state_label = (e.state or "pending").replace("_", " ")
|
||||
entry_badges.append(sx_call("events-calendar-entry-badge",
|
||||
bg_cls=bg_cls, name=e.name,
|
||||
state_label=state_label))
|
||||
badges.append({
|
||||
"bg-cls": bg_cls, "name": e.name,
|
||||
"state-label": (e.state or "pending").replace("_", " "),
|
||||
})
|
||||
if badges:
|
||||
cell["badges"] = badges
|
||||
|
||||
badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else ""
|
||||
cells.append(sx_call("events-calendar-cell",
|
||||
cell_cls=cell_cls, day_short=SxExpr(day_short_html),
|
||||
day_num=SxExpr(day_num_html),
|
||||
badges=SxExpr(badges_html) if badges_html else None))
|
||||
cells_data.append(cell)
|
||||
|
||||
cells_html = "(<> " + "".join(cells) + ")"
|
||||
arrows_html = "(<> " + "".join(nav_arrows) + ")"
|
||||
wd_html = "(<> " + wd_html + ")"
|
||||
return sx_call("events-calendar-grid",
|
||||
arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html),
|
||||
cells=SxExpr(cells_html))
|
||||
return sx_call("events-calendar-grid-from-data",
|
||||
pill_cls=pill_cls, month_name=month_name, year=str(year),
|
||||
prev_year_href=nav_link(prev_year, month),
|
||||
prev_month_href=nav_link(prev_month_year, prev_month),
|
||||
next_month_href=nav_link(next_month_year, next_month),
|
||||
next_year_href=nav_link(next_year, month),
|
||||
weekday_names=weekday_names or None,
|
||||
cells=cells_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -466,7 +402,7 @@ def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _day_main_panel_html(ctx: dict) -> str:
|
||||
"""Render the day entries table + add button."""
|
||||
"""Render the day entries table via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
|
||||
calendar = ctx.get("calendar")
|
||||
@@ -477,21 +413,49 @@ def _day_main_panel_html(ctx: dict) -> str:
|
||||
day = ctx.get("day")
|
||||
month = ctx.get("month")
|
||||
year = ctx.get("year")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
styles = ctx.get("styles") or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
||||
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
||||
|
||||
rows_html = ""
|
||||
if day_entries:
|
||||
row_parts = []
|
||||
for entry in day_entries:
|
||||
row_parts.append(_day_row_html(ctx, entry))
|
||||
rows_html = "".join(row_parts)
|
||||
else:
|
||||
rows_html = sx_call("events-day-empty-row")
|
||||
rows_data = []
|
||||
for entry in day_entries:
|
||||
entry_href = url_for(
|
||||
"defpage_entry_detail",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
|
||||
)
|
||||
row = {
|
||||
"href": entry_href, "name": entry.name,
|
||||
"state-id": f"entry-state-{entry.id}",
|
||||
"state": getattr(entry, "state", "pending") or "pending",
|
||||
}
|
||||
|
||||
# Slot/Time
|
||||
slot = getattr(entry, "slot", None)
|
||||
if slot:
|
||||
row["slot-name"] = slot.name
|
||||
row["slot-href"] = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
|
||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
||||
row["slot-time"] = f"({time_start}{time_end})"
|
||||
else:
|
||||
row["start"] = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
row["end"] = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
|
||||
# Cost
|
||||
cost = getattr(entry, "cost", None)
|
||||
row["cost-str"] = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
|
||||
# Tickets
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
if tp is not None:
|
||||
tc = getattr(entry, "ticket_count", None)
|
||||
row["has-tickets"] = True
|
||||
row["price-str"] = f"\u00a3{tp:.2f}"
|
||||
row["count-str"] = f"{tc} tickets" if tc is not None else "Unlimited"
|
||||
|
||||
rows_data.append(row)
|
||||
|
||||
add_url = url_for(
|
||||
"calendar.day.calendar_entries.add_form",
|
||||
@@ -499,74 +463,10 @@ def _day_main_panel_html(ctx: dict) -> str:
|
||||
day=day, month=month, year=year,
|
||||
)
|
||||
|
||||
return sx_call("events-day-table",
|
||||
list_container=list_container, rows=SxExpr(rows_html),
|
||||
pre_action=pre_action, add_url=add_url)
|
||||
|
||||
|
||||
def _day_row_html(ctx: dict, entry) -> str:
|
||||
"""Render a single day table row."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day = ctx.get("day")
|
||||
month = ctx.get("month")
|
||||
year = ctx.get("year")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
styles = ctx.get("styles") or {}
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
||||
|
||||
entry_href = url_for(
|
||||
"defpage_entry_detail",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
|
||||
)
|
||||
|
||||
# Name
|
||||
name_html = sx_call("events-day-row-name",
|
||||
href=entry_href, pill_cls=pill_cls, name=entry.name)
|
||||
|
||||
# Slot/Time
|
||||
slot = getattr(entry, "slot", None)
|
||||
if slot:
|
||||
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
|
||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
||||
slot_html = sx_call("events-day-row-slot",
|
||||
href=slot_href, pill_cls=pill_cls, slot_name=slot.name,
|
||||
time_str=f"({time_start}{time_end})")
|
||||
else:
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
slot_html = sx_call("events-day-row-time", start=start, end=end)
|
||||
|
||||
# State
|
||||
state = getattr(entry, "state", "pending") or "pending"
|
||||
state_badge = _entry_state_badge_html(state)
|
||||
state_td = sx_call("events-day-row-state",
|
||||
state_id=f"entry-state-{entry.id}", badge=state_badge)
|
||||
|
||||
# Cost
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
cost_td = sx_call("events-day-row-cost", cost_str=cost_str)
|
||||
|
||||
# Tickets
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
if tp is not None:
|
||||
tc = getattr(entry, "ticket_count", None)
|
||||
tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
|
||||
tickets_td = sx_call("events-day-row-tickets",
|
||||
price_str=f"\u00a3{tp:.2f}", count_str=tc_str)
|
||||
else:
|
||||
tickets_td = sx_call("events-day-row-no-tickets")
|
||||
|
||||
actions_td = sx_call("events-day-row-actions")
|
||||
|
||||
return sx_call("events-day-row",
|
||||
tr_cls=tr_cls, name=name_html, slot=slot_html,
|
||||
state=state_td, cost=cost_td,
|
||||
tickets=tickets_td, actions=actions_td)
|
||||
return sx_call("events-day-table-from-data",
|
||||
list_container=list_container, pre_action=pre_action,
|
||||
add_url=add_url, tr_cls=tr_cls, pill_cls=pill_cls,
|
||||
rows=rows_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -614,7 +514,7 @@ def _calendar_description_display_html(calendar, edit_url: str) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _markets_main_panel_html(ctx: dict) -> str:
|
||||
"""Render markets list + create form panel."""
|
||||
"""Render markets list + create form panel via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
@@ -623,48 +523,29 @@ def _markets_main_panel_html(ctx: dict) -> str:
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
markets = ctx.get("markets") or []
|
||||
|
||||
form_html = ""
|
||||
if can_create:
|
||||
create_url = url_for("markets.create_market")
|
||||
form_html = sx_call("crud-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
errors_id="market-create-errors", list_id="markets-list",
|
||||
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market")
|
||||
|
||||
list_html = _markets_list_html(ctx, markets)
|
||||
return sx_call("crud-panel",
|
||||
form=SxExpr(form_html), list=SxExpr(list_html),
|
||||
list_id="markets-list")
|
||||
|
||||
|
||||
def _markets_list_html(ctx: dict, markets: list) -> str:
|
||||
"""Render markets list items."""
|
||||
from quart import url_for
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
|
||||
if not markets:
|
||||
return sx_call("empty-state", message="No markets yet. Create one above.",
|
||||
cls="text-gray-500 mt-4")
|
||||
|
||||
parts = []
|
||||
items_data = []
|
||||
for m in markets:
|
||||
m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "")
|
||||
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
||||
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
||||
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
parts.append(sx_call("crud-item",
|
||||
href=market_href, name=m_name,
|
||||
slug=m_slug, del_url=del_url,
|
||||
csrf_hdr=csrf_hdr,
|
||||
list_id="markets-list",
|
||||
confirm_title="Delete market?",
|
||||
confirm_text="Products will be hidden (soft delete)"))
|
||||
return "".join(parts)
|
||||
items_data.append({
|
||||
"href": call_url(ctx, "market_url", f"/{slug}/{m_slug}/"),
|
||||
"name": m_name, "slug": m_slug,
|
||||
"del-url": url_for("markets.delete_market", market_slug=m_slug),
|
||||
"csrf-hdr": f'{{"X-CSRFToken":"{csrf}"}}',
|
||||
"confirm-title": "Delete market?",
|
||||
"confirm-text": "Products will be hidden (soft delete)",
|
||||
})
|
||||
|
||||
return sx_call("events-crud-panel-from-data",
|
||||
can_create=can_create or None,
|
||||
create_url=url_for("markets.create_market") if can_create else None,
|
||||
csrf=csrf, errors_id="market-create-errors", list_id="markets-list",
|
||||
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market",
|
||||
items=items_data or None,
|
||||
empty_msg="No markets yet. Create one above.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
"""Entry panels, cards, forms, edit/add."""
|
||||
from __future__ import annotations
|
||||
|
||||
from markupsafe import escape
|
||||
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
from .utils import (
|
||||
_entry_state_badge_html, _ticket_state_badge_html,
|
||||
_entry_state_badge_html,
|
||||
_list_container, _view_toggle_html,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# All events / page summary entry cards
|
||||
# All events / page summary entry cards — data extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
|
||||
def _entry_card_data(entry, page_info: dict, pending_tickets: dict,
|
||||
ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
|
||||
post: dict | None = None) -> str:
|
||||
"""Render a list card for one event entry."""
|
||||
from .tickets import _ticket_widget_html
|
||||
post: dict | None = None) -> dict:
|
||||
"""Extract data for a single entry card (list or tile)."""
|
||||
pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
|
||||
if is_page_scoped and post:
|
||||
page_slug = pi.get("slug", post.get("slug", ""))
|
||||
@@ -33,145 +30,103 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
|
||||
day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
|
||||
entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""
|
||||
|
||||
# Title (linked or plain)
|
||||
if entry_href:
|
||||
title_html = sx_call("events-entry-title-linked",
|
||||
href=entry_href, name=entry.name)
|
||||
else:
|
||||
title_html = sx_call("events-entry-title-plain", name=entry.name)
|
||||
|
||||
# Badges
|
||||
badges_html = ""
|
||||
# Page badge (only show if different from current page title)
|
||||
page_badge_href = ""
|
||||
page_badge_title = ""
|
||||
if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
|
||||
page_href = events_url_fn(f"/{page_slug}/")
|
||||
badges_html += sx_call("events-entry-page-badge",
|
||||
href=page_href, title=page_title)
|
||||
cal_name = getattr(entry, "calendar_name", "")
|
||||
if cal_name:
|
||||
badges_html += sx_call("events-entry-cal-badge", name=cal_name)
|
||||
page_badge_href = events_url_fn(f"/{page_slug}/")
|
||||
page_badge_title = page_title
|
||||
|
||||
# Time line
|
||||
time_parts = ""
|
||||
if day_href and not is_page_scoped:
|
||||
time_parts += sx_call("events-entry-time-linked",
|
||||
href=day_href,
|
||||
date_str=entry.start_at.strftime("%a %-d %b"))
|
||||
elif not is_page_scoped:
|
||||
time_parts += sx_call("events-entry-time-plain",
|
||||
date_str=entry.start_at.strftime("%a %-d %b"))
|
||||
time_parts += entry.start_at.strftime("%H:%M")
|
||||
if entry.end_at:
|
||||
time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}'
|
||||
cal_name = getattr(entry, "calendar_name", "") or ""
|
||||
|
||||
# Time parts
|
||||
date_str = entry.start_at.strftime("%a %-d %b") if entry.start_at else ""
|
||||
start_time = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end_time = entry.end_at.strftime("%H:%M") if entry.end_at else ""
|
||||
|
||||
# Tile time string (combined)
|
||||
time_str_parts = []
|
||||
if date_str:
|
||||
time_str_parts.append(date_str)
|
||||
if start_time:
|
||||
time_str_parts.append(start_time)
|
||||
time_str = " \u00b7 ".join(time_str_parts)
|
||||
if end_time:
|
||||
time_str += f" \u2013 {end_time}"
|
||||
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_html = sx_call("events-entry-cost",
|
||||
cost=f"\u00a3{cost:.2f}") if cost else ""
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost else None
|
||||
|
||||
# Ticket widget
|
||||
# Ticket widget data
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
widget_html = ""
|
||||
if tp is not None:
|
||||
has_ticket = tp is not None
|
||||
ticket_data = None
|
||||
if has_ticket:
|
||||
qty = pending_tickets.get(entry.id, 0)
|
||||
widget_html = sx_call("events-entry-widget-wrapper",
|
||||
widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
|
||||
ticket_data = {
|
||||
"entry-id": str(entry.id),
|
||||
"price": f"\u00a3{tp:.2f}",
|
||||
"qty": qty,
|
||||
"ticket-url": ticket_url,
|
||||
"csrf": _get_csrf(),
|
||||
}
|
||||
|
||||
return sx_call("events-entry-card",
|
||||
title=title_html, badges=SxExpr(badges_html),
|
||||
time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
|
||||
widget=SxExpr(widget_html))
|
||||
return {
|
||||
"entry-href": entry_href or None,
|
||||
"name": entry.name,
|
||||
"day-href": day_href or None,
|
||||
"page-badge-href": page_badge_href or None,
|
||||
"page-badge-title": page_badge_title or None,
|
||||
"cal-name": cal_name or None,
|
||||
"date-str": date_str,
|
||||
"start-time": start_time,
|
||||
"end-time": end_time or None,
|
||||
"time-str": time_str,
|
||||
"is-page-scoped": is_page_scoped or None,
|
||||
"cost": cost_str,
|
||||
"has-ticket": has_ticket or None,
|
||||
"ticket-data": ticket_data,
|
||||
}
|
||||
|
||||
|
||||
def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
|
||||
ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
|
||||
post: dict | None = None) -> str:
|
||||
"""Render a tile card for one event entry."""
|
||||
from .tickets import _ticket_widget_html
|
||||
pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
|
||||
if is_page_scoped and post:
|
||||
page_slug = pi.get("slug", post.get("slug", ""))
|
||||
else:
|
||||
page_slug = pi.get("slug", "")
|
||||
page_title = pi.get("title")
|
||||
def _get_csrf() -> str:
|
||||
"""Get CSRF token (lazy import)."""
|
||||
try:
|
||||
from flask_wtf.csrf import generate_csrf
|
||||
return generate_csrf()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
day_href = ""
|
||||
if page_slug and entry.start_at:
|
||||
day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
|
||||
entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""
|
||||
|
||||
# Title
|
||||
if entry_href:
|
||||
title_html = sx_call("events-entry-title-tile-linked",
|
||||
href=entry_href, name=entry.name)
|
||||
else:
|
||||
title_html = sx_call("events-entry-title-tile-plain", name=entry.name)
|
||||
|
||||
# Badges
|
||||
badges_html = ""
|
||||
if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
|
||||
page_href = events_url_fn(f"/{page_slug}/")
|
||||
badges_html += sx_call("events-entry-page-badge",
|
||||
href=page_href, title=page_title)
|
||||
cal_name = getattr(entry, "calendar_name", "")
|
||||
if cal_name:
|
||||
badges_html += sx_call("events-entry-cal-badge", name=cal_name)
|
||||
|
||||
# Time
|
||||
time_html = ""
|
||||
if day_href:
|
||||
time_html += (sx_call("events-entry-time-linked",
|
||||
href=day_href,
|
||||
date_str=entry.start_at.strftime("%a %-d %b"))).replace(" · ", "")
|
||||
else:
|
||||
time_html += entry.start_at.strftime("%a %-d %b")
|
||||
time_html += f' \u00b7 {entry.start_at.strftime("%H:%M")}'
|
||||
if entry.end_at:
|
||||
time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}'
|
||||
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_html = sx_call("events-entry-cost",
|
||||
cost=f"\u00a3{cost:.2f}") if cost else ""
|
||||
|
||||
# Ticket widget
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
widget_html = ""
|
||||
if tp is not None:
|
||||
qty = pending_tickets.get(entry.id, 0)
|
||||
widget_html = sx_call("events-entry-tile-widget-wrapper",
|
||||
widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
|
||||
|
||||
return sx_call("events-entry-card-tile",
|
||||
title=title_html, badges=SxExpr(badges_html),
|
||||
time=SxExpr(time_html), cost=SxExpr(cost_html),
|
||||
widget=SxExpr(widget_html))
|
||||
def _entry_cards_data(entries, page_info, pending_tickets, ticket_url,
|
||||
events_url_fn, view, *, is_page_scoped=False, post=None) -> list:
|
||||
"""Extract data list for entry cards with date separators."""
|
||||
items = []
|
||||
last_date = None
|
||||
for entry in entries:
|
||||
if view != "tile":
|
||||
entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
|
||||
if entry_date != last_date:
|
||||
items.append({"is-separator": True, "date-str": entry_date})
|
||||
last_date = entry_date
|
||||
items.append(_entry_card_data(
|
||||
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
))
|
||||
return items
|
||||
|
||||
|
||||
def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
|
||||
events_url_fn, view, page, has_more, next_url,
|
||||
*, is_page_scoped=False, post=None) -> str:
|
||||
"""Render entry cards (list or tile) with sentinel."""
|
||||
parts = []
|
||||
last_date = None
|
||||
for entry in entries:
|
||||
if view == "tile":
|
||||
parts.append(_entry_card_tile_html(
|
||||
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
))
|
||||
else:
|
||||
entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
|
||||
if entry_date != last_date:
|
||||
parts.append(sx_call("events-date-separator",
|
||||
date_str=entry_date))
|
||||
last_date = entry_date
|
||||
parts.append(_entry_card_html(
|
||||
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
))
|
||||
|
||||
if has_more:
|
||||
parts.append(sx_call("sentinel-simple",
|
||||
id=f"sentinel-{page}", next_url=next_url))
|
||||
return "".join(parts)
|
||||
"""Render entry cards via sx defcomp with data extraction."""
|
||||
items = _entry_cards_data(
|
||||
entries, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
view, is_page_scoped=is_page_scoped, post=post,
|
||||
)
|
||||
return sx_call("events-entry-cards-from-data",
|
||||
items=items, view=view, page=page,
|
||||
has_more=has_more, next_url=next_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -183,23 +138,15 @@ def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_
|
||||
*, is_page_scoped=False, post=None) -> str:
|
||||
"""Render the events main panel with view toggle + cards."""
|
||||
toggle = _view_toggle_html(ctx, view)
|
||||
|
||||
items = None
|
||||
if entries:
|
||||
cards = _entry_cards_html(
|
||||
items = _entry_cards_data(
|
||||
entries, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
view, page, has_more, next_url,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
view, is_page_scoped=is_page_scoped, post=post,
|
||||
)
|
||||
grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||
if view == "tile" else "max-w-full px-3 py-3 space-y-3")
|
||||
body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
|
||||
else:
|
||||
body = sx_call("empty-state", icon="fa fa-calendar-xmark",
|
||||
message="No upcoming events",
|
||||
cls="px-3 py-12 text-center text-stone-400")
|
||||
|
||||
return sx_call("events-main-panel-body",
|
||||
toggle=toggle, body=body)
|
||||
return sx_call("events-main-panel-from-data",
|
||||
toggle=toggle, items=items, view=view, page=page,
|
||||
has_more=has_more, next_url=next_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -373,27 +320,16 @@ def _entry_nav_html(ctx: dict) -> str:
|
||||
entry_posts = ctx.get("entry_posts") or []
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
|
||||
blog_url_fn = ctx.get("blog_url")
|
||||
|
||||
parts = []
|
||||
|
||||
# Associated Posts scrolling menu
|
||||
# Associated Posts scrolling menu (strip OOB attr for inline embedding)
|
||||
if entry_posts:
|
||||
post_links = ""
|
||||
for ep in entry_posts:
|
||||
slug = getattr(ep, "slug", "")
|
||||
title = getattr(ep, "title", "")
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
|
||||
if feat:
|
||||
img_html = sx_call("events-post-img", src=feat, alt=title)
|
||||
else:
|
||||
img_html = sx_call("events-post-img-placeholder")
|
||||
post_links += sx_call("events-entry-nav-post-link",
|
||||
href=href, img=img_html, title=title)
|
||||
parts.append((sx_call("events-entry-posts-nav-oob",
|
||||
items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', ''))
|
||||
posts_data = _entry_posts_nav_data(entry_posts, blog_url_fn)
|
||||
nav_html = sx_call("events-entry-posts-nav-inner-from-data", posts=posts_data or None)
|
||||
if nav_html:
|
||||
parts.append(nav_html.replace(' :hx-swap-oob "true"', ''))
|
||||
|
||||
# Admin link
|
||||
if is_admin:
|
||||
@@ -432,7 +368,7 @@ def _entry_title_html(entry) -> str:
|
||||
|
||||
|
||||
def _entry_options_html(entry, calendar, day, month, year) -> str:
|
||||
"""Render confirm/decline/provisional buttons based on entry state."""
|
||||
"""Render confirm/decline/provisional buttons via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -443,39 +379,30 @@ def _entry_options_html(entry, calendar, day, month, year) -> str:
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
state = getattr(entry, "state", "pending") or "pending"
|
||||
target = f"#calendar_entry_options_{eid}"
|
||||
|
||||
def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
|
||||
url = url_for(
|
||||
f"calendar.day.calendar_entries.calendar_entry.{action_name}",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
|
||||
)
|
||||
btn_type = "button" if trigger_type == "button" else "submit"
|
||||
return sx_call("events-entry-option-button",
|
||||
url=url, target=target, csrf=csrf, btn_type=btn_type,
|
||||
action_btn=action_btn, confirm_title=confirm_title,
|
||||
confirm_text=confirm_text, label=label,
|
||||
is_btn=trigger_type == "button")
|
||||
def _btn_data(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
|
||||
return {
|
||||
"url": url_for(f"calendar.day.calendar_entries.calendar_entry.{action_name}",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid),
|
||||
"csrf": csrf, "btn-type": "button" if trigger_type == "button" else "submit",
|
||||
"action-btn": action_btn, "confirm-title": confirm_title,
|
||||
"confirm-text": confirm_text, "label": label,
|
||||
"is-btn": True if trigger_type == "button" else None,
|
||||
}
|
||||
|
||||
buttons_html = ""
|
||||
buttons = []
|
||||
if state == "provisional":
|
||||
buttons_html += _make_button(
|
||||
"confirm_entry", "confirm",
|
||||
"Confirm entry?", "Are you sure you want to confirm this entry?",
|
||||
)
|
||||
buttons_html += _make_button(
|
||||
"decline_entry", "decline",
|
||||
"Decline entry?", "Are you sure you want to decline this entry?",
|
||||
)
|
||||
buttons.append(_btn_data("confirm_entry", "confirm",
|
||||
"Confirm entry?", "Are you sure you want to confirm this entry?"))
|
||||
buttons.append(_btn_data("decline_entry", "decline",
|
||||
"Decline entry?", "Are you sure you want to decline this entry?"))
|
||||
elif state == "confirmed":
|
||||
buttons_html += _make_button(
|
||||
"provisional_entry", "provisional",
|
||||
"Provisional entry?", "Are you sure you want to provisional this entry?",
|
||||
trigger_type="button",
|
||||
)
|
||||
buttons.append(_btn_data("provisional_entry", "provisional",
|
||||
"Provisional entry?", "Are you sure you want to provisional this entry?",
|
||||
trigger_type="button"))
|
||||
|
||||
return sx_call("events-entry-options",
|
||||
entry_id=str(eid), buttons=SxExpr(buttons_html))
|
||||
return sx_call("events-entry-options-from-data",
|
||||
entry_id=str(eid), buttons=buttons or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -525,7 +452,7 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
|
||||
"""Render associated posts list with remove buttons and search input."""
|
||||
"""Render associated posts list via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -533,38 +460,46 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
eid_s = str(eid)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
|
||||
posts_html = ""
|
||||
posts_data = []
|
||||
if entry_posts:
|
||||
items = ""
|
||||
for ep in entry_posts:
|
||||
ep_title = getattr(ep, "title", "")
|
||||
ep_id = getattr(ep, "id", 0)
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
img_html = (sx_call("events-post-img", src=feat, alt=ep_title)
|
||||
if feat else sx_call("events-post-img-placeholder"))
|
||||
|
||||
del_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.remove_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid, post_id=ep_id,
|
||||
)
|
||||
items += sx_call("events-entry-post-item",
|
||||
img=img_html, title=ep_title,
|
||||
del_url=del_url, entry_id=eid_s,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
|
||||
else:
|
||||
posts_html = sx_call("events-entry-posts-none")
|
||||
posts_data.append({
|
||||
"title": getattr(ep, "title", ""),
|
||||
"img": getattr(ep, "feature_image", None),
|
||||
"del-url": url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.remove_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid, post_id=getattr(ep, "id", 0),
|
||||
),
|
||||
"csrf-hdr": csrf_hdr,
|
||||
})
|
||||
|
||||
search_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.search_posts",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
|
||||
)
|
||||
|
||||
return sx_call("events-entry-posts-panel",
|
||||
posts=posts_html, search_url=search_url,
|
||||
entry_id=eid_s)
|
||||
return sx_call("events-entry-posts-panel-from-data",
|
||||
entry_id=eid_s, posts=posts_data or None,
|
||||
search_url=search_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry posts nav data helper (shared by nav OOB + entry nav)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _entry_posts_nav_data(entry_posts, blog_url_fn) -> list:
|
||||
"""Extract post nav data from ORM entry posts."""
|
||||
if not entry_posts:
|
||||
return []
|
||||
return [
|
||||
{"href": blog_url_fn(f"/{getattr(ep, 'slug', '')}/") if blog_url_fn else f"/{getattr(ep, 'slug', '')}/",
|
||||
"title": getattr(ep, "title", ""),
|
||||
"img": getattr(ep, "feature_image", None)}
|
||||
for ep in entry_posts
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -572,28 +507,15 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_entry_posts_nav_oob(entry_posts) -> str:
|
||||
"""Render OOB nav for entry posts (scrolling menu)."""
|
||||
"""Render OOB nav for entry posts via data extraction + sx defcomp."""
|
||||
from quart import g
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
|
||||
blog_url_fn = getattr(g, "blog_url", None)
|
||||
|
||||
if not entry_posts:
|
||||
return sx_call("events-entry-posts-nav-oob-empty")
|
||||
|
||||
items = ""
|
||||
for ep in entry_posts:
|
||||
slug = getattr(ep, "slug", "")
|
||||
title = getattr(ep, "title", "")
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
|
||||
img_html = (sx_call("events-post-img", src=feat, alt=title)
|
||||
if feat else sx_call("events-post-img-placeholder"))
|
||||
items += sx_call("events-entry-nav-post",
|
||||
href=href, nav_btn=nav_btn,
|
||||
img=img_html, title=title)
|
||||
|
||||
return sx_call("events-entry-posts-nav-oob", items=SxExpr(items))
|
||||
posts_data = _entry_posts_nav_data(entry_posts, blog_url_fn)
|
||||
return sx_call("events-entry-posts-nav-oob-from-data",
|
||||
nav_btn=nav_btn, posts=posts_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -601,31 +523,28 @@ def render_entry_posts_nav_oob(entry_posts) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
|
||||
"""Render OOB nav for confirmed entries in a day."""
|
||||
"""Render OOB nav for confirmed entries via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
if not confirmed_entries:
|
||||
return sx_call("events-day-entries-nav-oob-empty")
|
||||
entries_data = []
|
||||
if confirmed_entries:
|
||||
for entry in confirmed_entries:
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
entries_data.append({
|
||||
"href": url_for("defpage_entry_detail", calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
entry_id=entry.id),
|
||||
"name": entry.name,
|
||||
"time-str": start + end,
|
||||
})
|
||||
|
||||
items = ""
|
||||
for entry in confirmed_entries:
|
||||
href = url_for(
|
||||
"defpage_entry_detail",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
entry_id=entry.id,
|
||||
)
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
items += sx_call("events-day-nav-entry",
|
||||
href=href, nav_btn=nav_btn,
|
||||
name=entry.name, time_str=start + end)
|
||||
|
||||
return sx_call("events-day-entries-nav-oob", items=SxExpr(items))
|
||||
return sx_call("events-day-entries-nav-oob-from-data",
|
||||
nav_btn=nav_btn, entries=entries_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -633,7 +552,7 @@ def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
||||
"""Render OOB nav for associated entries and calendars of a post."""
|
||||
"""Render OOB nav for associated entries and calendars via data + sx defcomp."""
|
||||
from quart import g
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
@@ -641,14 +560,9 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
||||
nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "")
|
||||
|
||||
has_entries = associated_entries and getattr(associated_entries, "entries", None)
|
||||
has_items = has_entries or calendars
|
||||
|
||||
if not has_items:
|
||||
return sx_call("events-post-nav-oob-empty")
|
||||
|
||||
slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")
|
||||
|
||||
items = ""
|
||||
entries_data = []
|
||||
if has_entries:
|
||||
for entry in associated_entries.entries:
|
||||
entry_path = (
|
||||
@@ -656,27 +570,31 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
||||
f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/"
|
||||
f"entries/{entry.id}/"
|
||||
)
|
||||
href = events_url(entry_path)
|
||||
time_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
|
||||
end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
items += sx_call("events-post-nav-entry",
|
||||
href=href, nav_btn=nav_btn,
|
||||
name=entry.name, time_str=time_str + end_str)
|
||||
entries_data.append({
|
||||
"href": events_url(entry_path),
|
||||
"name": entry.name,
|
||||
"time-str": time_str + end_str,
|
||||
})
|
||||
|
||||
calendars_data = []
|
||||
if calendars:
|
||||
for cal in calendars:
|
||||
cs = getattr(cal, "slug", "")
|
||||
local_href = events_url(f"/{slug}/{cs}/")
|
||||
items += sx_call("events-post-nav-calendar",
|
||||
href=local_href, nav_btn=nav_btn, name=cal.name)
|
||||
calendars_data.append({
|
||||
"href": events_url(f"/{slug}/{cs}/"),
|
||||
"name": cal.name,
|
||||
})
|
||||
|
||||
hs = ("on load or scroll "
|
||||
"if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth "
|
||||
"remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow "
|
||||
"else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end")
|
||||
|
||||
return sx_call("events-post-nav-wrapper",
|
||||
items=SxExpr(items), hyperscript=hs)
|
||||
return sx_call("events-post-nav-wrapper-from-data",
|
||||
nav_btn=nav_btn, entries=entries_data or None,
|
||||
calendars=calendars_data or None, hyperscript=hs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -800,42 +718,36 @@ def _entry_admin_main_panel_html(ctx: dict) -> str:
|
||||
|
||||
def render_post_search_results(search_posts, search_query, page, total_pages,
|
||||
entry, calendar, day, month, year) -> str:
|
||||
"""Render post search results (replaces _types/entry/_post_search_results.html)."""
|
||||
"""Render post search results via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
post_url = url_for("calendar.day.calendar_entries.calendar_entry.add_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid)
|
||||
|
||||
parts = []
|
||||
items_data = []
|
||||
for sp in search_posts:
|
||||
post_url = url_for("calendar.day.calendar_entries.calendar_entry.add_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid)
|
||||
feat = getattr(sp, "feature_image", None)
|
||||
title = getattr(sp, "title", "")
|
||||
if feat:
|
||||
img_html = sx_call("events-post-img", src=feat, alt=title)
|
||||
else:
|
||||
img_html = sx_call("events-post-img-placeholder")
|
||||
items_data.append({
|
||||
"post-url": post_url, "entry-id": str(eid),
|
||||
"csrf": csrf, "post-id": str(sp.id),
|
||||
"img": getattr(sp, "feature_image", None),
|
||||
"title": getattr(sp, "title", ""),
|
||||
})
|
||||
|
||||
parts.append(sx_call("events-post-search-item",
|
||||
post_url=post_url, entry_id=str(eid), csrf=csrf,
|
||||
post_id=str(sp.id), img=img_html, title=title))
|
||||
|
||||
result = "".join(parts)
|
||||
|
||||
if page < int(total_pages):
|
||||
has_more = page < int(total_pages)
|
||||
next_url = None
|
||||
if has_more:
|
||||
next_url = url_for("calendar.day.calendar_entries.calendar_entry.search_posts",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid, q=search_query, page=page + 1)
|
||||
result += sx_call("events-post-search-sentinel",
|
||||
page=str(page), next_url=next_url)
|
||||
elif search_posts:
|
||||
result += sx_call("events-post-search-end")
|
||||
|
||||
return result
|
||||
return sx_call("events-post-search-results-from-data",
|
||||
items=items_data or None, page=str(page),
|
||||
next_url=next_url, has_more=has_more or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -846,7 +758,7 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
|
||||
"""Render entry edit form (replaces _types/entry/_edit.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from .slots import _slot_options_html, _SLOT_PICKER_JS
|
||||
from .slots import _slot_options_data, _SLOT_PICKER_JS
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
@@ -862,12 +774,9 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)
|
||||
|
||||
# Slot picker
|
||||
if day_slots:
|
||||
options_html = _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None))
|
||||
slot_picker_html = sx_call("events-slot-picker",
|
||||
id=f"entry-slot-{eid}", options=SxExpr(options_html))
|
||||
else:
|
||||
slot_picker_html = sx_call("events-no-slots")
|
||||
slots_data = _slot_options_data(day_slots, selected_slot_id=getattr(entry, "slot_id", None)) if day_slots else []
|
||||
slot_picker_html = sx_call("events-slot-picker-from-data",
|
||||
id=f"entry-slot-{eid}", slots=slots_data or None)
|
||||
|
||||
# Values
|
||||
start_val = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
@@ -897,7 +806,7 @@ def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
|
||||
"""Render entry add form (replaces _types/day/_add.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from .slots import _slot_options_html, _SLOT_PICKER_JS
|
||||
from .slots import _slot_options_data, _SLOT_PICKER_JS
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
@@ -911,12 +820,9 @@ def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year)
|
||||
|
||||
# Slot picker
|
||||
if day_slots:
|
||||
options_html = _slot_options_html(day_slots)
|
||||
slot_picker_html = sx_call("events-slot-picker",
|
||||
id="entry-slot-new", options=SxExpr(options_html))
|
||||
else:
|
||||
slot_picker_html = sx_call("events-no-slots")
|
||||
slots_data = _slot_options_data(day_slots) if day_slots else []
|
||||
slot_picker_html = sx_call("events-slot-picker-from-data",
|
||||
id="entry-slot-new", slots=slots_data or None)
|
||||
|
||||
html = sx_call("events-entry-add-form",
|
||||
post_url=post_url, csrf=csrf,
|
||||
@@ -944,34 +850,33 @@ def render_entry_add_button(calendar, day, month, year) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
|
||||
"""Render container cards entries (replaces fragments/container_cards_entries.html)."""
|
||||
"""Render container cards entries via data extraction + sx defcomp."""
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
parts = []
|
||||
widgets_data = []
|
||||
for post_id in post_ids:
|
||||
parts.append(f"<!-- card-widget:{post_id} -->")
|
||||
widget_entries = batch.get(post_id, [])
|
||||
if widget_entries:
|
||||
cards_html = ""
|
||||
for entry in widget_entries:
|
||||
_post_slug = slug_map.get(post_id, "")
|
||||
_entry_path = (
|
||||
f"/{_post_slug}/{entry.calendar_slug}/"
|
||||
f"{entry.start_at.year}/{entry.start_at.month}/"
|
||||
f"{entry.start_at.day}/entries/{entry.id}/"
|
||||
)
|
||||
time_str = entry.start_at.strftime("%H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
cards_html += sx_call("events-frag-entry-card",
|
||||
href=events_url(_entry_path),
|
||||
name=entry.name,
|
||||
date_str=entry.start_at.strftime("%a, %b %d"),
|
||||
time_str=time_str)
|
||||
parts.append(sx_call("events-frag-entries-widget", cards=SxExpr(cards_html)))
|
||||
parts.append(f"<!-- /card-widget:{post_id} -->")
|
||||
entries_data = []
|
||||
for entry in widget_entries:
|
||||
_post_slug = slug_map.get(post_id, "")
|
||||
_entry_path = (
|
||||
f"/{_post_slug}/{entry.calendar_slug}/"
|
||||
f"{entry.start_at.year}/{entry.start_at.month}/"
|
||||
f"{entry.start_at.day}/entries/{entry.id}/"
|
||||
)
|
||||
time_str = entry.start_at.strftime("%H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
entries_data.append({
|
||||
"href": events_url(_entry_path),
|
||||
"name": entry.name,
|
||||
"date-str": entry.start_at.strftime("%a, %b %d"),
|
||||
"time-str": time_str,
|
||||
})
|
||||
widgets_data.append({"entries": entries_data or None})
|
||||
|
||||
return "\n".join(parts)
|
||||
return sx_call("events-frag-container-cards-from-data",
|
||||
widgets=widgets_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -979,32 +884,23 @@ def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_fragment_account_tickets(tickets) -> str:
|
||||
"""Render account page tickets (replaces fragments/account_page_tickets.html)."""
|
||||
"""Render account page tickets via data extraction + sx defcomp."""
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
tickets_data = []
|
||||
if tickets:
|
||||
items_html = ""
|
||||
for ticket in tickets:
|
||||
href = events_url(f"/tickets/{ticket.code}/")
|
||||
date_str = ticket.entry_start_at.strftime("%d %b %Y, %H:%M")
|
||||
cal_name = ""
|
||||
if getattr(ticket, "calendar_name", None):
|
||||
cal_name = f'<span>· {escape(ticket.calendar_name)}</span>'
|
||||
type_name = ""
|
||||
if getattr(ticket, "ticket_type_name", None):
|
||||
type_name = f'<span>· {escape(ticket.ticket_type_name)}</span>'
|
||||
badge_html = sx_call("status-pill",
|
||||
status=getattr(ticket, "state", ""))
|
||||
items_html += sx_call("events-frag-ticket-item",
|
||||
href=href, entry_name=ticket.entry_name,
|
||||
date_str=date_str, calendar_name=cal_name,
|
||||
type_name=type_name, badge=badge_html)
|
||||
body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
|
||||
else:
|
||||
body = sx_call("empty-state", message="No tickets yet.",
|
||||
cls="text-sm text-stone-500")
|
||||
tickets_data.append({
|
||||
"href": events_url(f"/tickets/{ticket.code}/"),
|
||||
"entry-name": ticket.entry_name,
|
||||
"date-str": ticket.entry_start_at.strftime("%d %b %Y, %H:%M"),
|
||||
"calendar-name": getattr(ticket, "calendar_name", None) or None,
|
||||
"type-name": getattr(ticket, "ticket_type_name", None) or None,
|
||||
"state": getattr(ticket, "state", ""),
|
||||
})
|
||||
|
||||
return sx_call("events-frag-tickets-panel", items=body)
|
||||
return sx_call("events-frag-tickets-panel-from-data",
|
||||
tickets=tickets_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1012,31 +908,18 @@ def render_fragment_account_tickets(tickets) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_fragment_account_bookings(bookings) -> str:
|
||||
"""Render account page bookings (replaces fragments/account_page_bookings.html)."""
|
||||
"""Render account page bookings via data extraction + sx defcomp."""
|
||||
bookings_data = []
|
||||
if bookings:
|
||||
items_html = ""
|
||||
for booking in bookings:
|
||||
date_str = booking.start_at.strftime("%d %b %Y, %H:%M")
|
||||
if getattr(booking, "end_at", None):
|
||||
date_str_extra = f'<span>– {escape(booking.end_at.strftime("%H:%M"))}</span>'
|
||||
else:
|
||||
date_str_extra = ""
|
||||
cal_name = ""
|
||||
if getattr(booking, "calendar_name", None):
|
||||
cal_name = f'<span>· {escape(booking.calendar_name)}</span>'
|
||||
cost_str = ""
|
||||
if getattr(booking, "cost", None):
|
||||
cost_str = f'<span>· £{escape(str(booking.cost))}</span>'
|
||||
badge_html = sx_call("status-pill",
|
||||
status=getattr(booking, "state", ""))
|
||||
items_html += sx_call("events-frag-booking-item",
|
||||
name=booking.name,
|
||||
date_str=date_str + date_str_extra,
|
||||
calendar_name=cal_name, cost_str=cost_str,
|
||||
badge=badge_html)
|
||||
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
|
||||
else:
|
||||
body = sx_call("empty-state", message="No bookings yet.",
|
||||
cls="text-sm text-stone-500")
|
||||
bookings_data.append({
|
||||
"name": booking.name,
|
||||
"date-str": booking.start_at.strftime("%d %b %Y, %H:%M"),
|
||||
"end-time": booking.end_at.strftime("%H:%M") if getattr(booking, "end_at", None) else None,
|
||||
"calendar-name": getattr(booking, "calendar_name", None) or None,
|
||||
"cost-str": str(booking.cost) if getattr(booking, "cost", None) else None,
|
||||
"state": getattr(booking, "state", ""),
|
||||
})
|
||||
|
||||
return sx_call("events-frag-bookings-panel", items=body)
|
||||
return sx_call("events-frag-bookings-panel-from-data",
|
||||
bookings=bookings_data or None)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from shared.sx.helpers import (
|
||||
render_to_sx_with_env,
|
||||
render_to_sx_with_env, sx_call,
|
||||
post_admin_header_sx, oob_header_sx,
|
||||
header_child_sx, full_page_sx, oob_page_sx,
|
||||
)
|
||||
|
||||
from .utils import _clear_deeper_oob, _ensure_container_nav
|
||||
from .utils import _ensure_container_nav
|
||||
from .calendar import (
|
||||
_post_header_sx, _calendars_header_sx,
|
||||
_calendar_header_sx, _day_header_sx,
|
||||
@@ -129,7 +129,7 @@ async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets,
|
||||
)
|
||||
|
||||
oobs = await _post_header_sx(ctx, oob=True)
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child")
|
||||
oobs += sx_call("events-clear-deeper-post")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
@@ -172,8 +172,7 @@ async def render_calendars_oob(ctx: dict) -> str:
|
||||
ctx = await _ensure_container_nav(ctx)
|
||||
slug = (ctx.get("post") or {}).get("slug", "")
|
||||
oobs = await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||
"post-admin-row", "post-admin-header-child")
|
||||
oobs += sx_call("events-clear-deeper-post-admin")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
@@ -196,8 +195,7 @@ async def render_calendar_oob(ctx: dict) -> str:
|
||||
oobs = await _post_header_sx(ctx, oob=True)
|
||||
oobs += await oob_header_sx("post-header-child", "calendar-header-child",
|
||||
_calendar_header_sx(ctx))
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||
"calendar-row", "calendar-header-child")
|
||||
oobs += sx_call("events-clear-deeper-calendar")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
@@ -221,9 +219,7 @@ async def render_day_oob(ctx: dict) -> str:
|
||||
oobs = _calendar_header_sx(ctx, oob=True)
|
||||
oobs += await oob_header_sx("calendar-header-child", "day-header-child",
|
||||
_day_header_sx(ctx))
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||
"calendar-row", "calendar-header-child",
|
||||
"day-row", "day-header-child")
|
||||
oobs += sx_call("events-clear-deeper-day")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
@@ -111,9 +110,9 @@ _SLOT_PICKER_JS = """\
|
||||
# Slot options (shared by entry edit + add forms)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _slot_options_html(day_slots, selected_slot_id=None) -> str:
|
||||
"""Build slot <option> elements."""
|
||||
parts = []
|
||||
def _slot_options_data(day_slots, selected_slot_id=None) -> list:
|
||||
"""Extract slot option data for sx composition."""
|
||||
result = []
|
||||
for slot in day_slots:
|
||||
start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
@@ -127,16 +126,15 @@ def _slot_options_html(day_slots, selected_slot_id=None) -> str:
|
||||
label_parts.append("\u2013open-ended)")
|
||||
if flexible:
|
||||
label_parts.append("[flexible]")
|
||||
label = " ".join(label_parts)
|
||||
|
||||
parts.append(sx_call("events-slot-option",
|
||||
value=str(slot.id),
|
||||
data_start=start, data_end=end,
|
||||
data_flexible="1" if flexible else "0",
|
||||
data_cost=cost_str,
|
||||
selected="selected" if selected_slot_id == slot.id else None,
|
||||
label=label))
|
||||
return "".join(parts)
|
||||
result.append({
|
||||
"value": str(slot.id),
|
||||
"data-start": start, "data-end": end,
|
||||
"data-flexible": "1" if flexible else "0",
|
||||
"data-cost": cost_str,
|
||||
"selected": "selected" if selected_slot_id == slot.id else None,
|
||||
"label": " ".join(label_parts),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -169,7 +167,7 @@ def _slot_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
|
||||
"""Render slot detail view."""
|
||||
"""Render slot detail view via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
@@ -179,38 +177,23 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
|
||||
|
||||
days_display = getattr(slot, "days_display", "\u2014")
|
||||
days = days_display.split(", ")
|
||||
flexible = getattr(slot, "flexible", False)
|
||||
if days and days[0] == "\u2014":
|
||||
days = []
|
||||
|
||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
time_end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
cost = getattr(slot, "cost", None)
|
||||
cost_str = f"{cost:.2f}" if cost is not None else ""
|
||||
desc = getattr(slot, "description", "") or ""
|
||||
|
||||
edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug)
|
||||
|
||||
# Days pills
|
||||
if days and days[0] != "\u2014":
|
||||
days_inner = "".join(
|
||||
sx_call("events-slot-day-pill", day=d) for d in days
|
||||
)
|
||||
days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner))
|
||||
else:
|
||||
days_html = sx_call("events-slot-no-days")
|
||||
|
||||
sid = str(slot.id)
|
||||
|
||||
result = sx_call("events-slot-panel",
|
||||
slot_id=sid, list_container=list_container,
|
||||
days=days_html,
|
||||
flexible="yes" if flexible else "no",
|
||||
time_str=f"{time_start} \u2014 {time_end}",
|
||||
cost_str=cost_str,
|
||||
pre_action=pre_action, edit_url=edit_url)
|
||||
|
||||
if oob:
|
||||
result += sx_call("events-slot-description-oob", description=desc)
|
||||
|
||||
return result
|
||||
return sx_call("events-slot-panel-from-data",
|
||||
slot_id=str(slot.id), list_container=list_container,
|
||||
days=days or None,
|
||||
flexible="yes" if getattr(slot, "flexible", False) else "no",
|
||||
time_str=f"{time_start} \u2014 {time_end}",
|
||||
cost_str=f"{cost:.2f}" if cost is not None else "",
|
||||
pre_action=pre_action,
|
||||
edit_url=url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug),
|
||||
description=getattr(slot, "description", "") or "",
|
||||
oob=oob or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -218,7 +201,7 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_slots_table(slots, calendar) -> str:
|
||||
"""Render slots table with rows and add button."""
|
||||
"""Render slots table via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -232,46 +215,34 @@ def render_slots_table(slots, calendar) -> str:
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
rows_html = ""
|
||||
slots_data = []
|
||||
if slots:
|
||||
for s in slots:
|
||||
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id)
|
||||
del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
|
||||
desc = getattr(s, "description", "") or ""
|
||||
|
||||
days_display = getattr(s, "days_display", "\u2014")
|
||||
day_list = days_display.split(", ")
|
||||
if day_list and day_list[0] != "\u2014":
|
||||
days_inner = "".join(
|
||||
sx_call("events-slot-day-pill", day=d) for d in day_list
|
||||
)
|
||||
days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner))
|
||||
else:
|
||||
days_html = sx_call("events-slot-no-days")
|
||||
|
||||
if day_list and day_list[0] == "\u2014":
|
||||
day_list = []
|
||||
time_start = s.time_start.strftime("%H:%M") if s.time_start else ""
|
||||
time_end = s.time_end.strftime("%H:%M") if s.time_end else ""
|
||||
cost = getattr(s, "cost", None)
|
||||
cost_str = f"{cost:.2f}" if cost is not None else ""
|
||||
slots_data.append({
|
||||
"slot-href": url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id),
|
||||
"slot-name": s.name,
|
||||
"description": getattr(s, "description", "") or "",
|
||||
"flexible": "yes" if s.flexible else "no",
|
||||
"days": day_list or None,
|
||||
"time-str": f"{time_start} - {time_end}",
|
||||
"cost-str": f"{cost:.2f}" if cost is not None else "",
|
||||
"del-url": url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id),
|
||||
})
|
||||
|
||||
rows_html += sx_call("events-slots-row",
|
||||
tr_cls=tr_cls, slot_href=slot_href,
|
||||
pill_cls=pill_cls, hx_select=hx_select,
|
||||
slot_name=s.name, description=desc,
|
||||
flexible="yes" if s.flexible else "no",
|
||||
days=days_html,
|
||||
time_str=f"{time_start} - {time_end}",
|
||||
cost_str=cost_str, action_btn=action_btn,
|
||||
del_url=del_url,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
else:
|
||||
rows_html = sx_call("events-slots-empty-row")
|
||||
|
||||
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
||||
|
||||
return sx_call("events-slots-table",
|
||||
list_container=list_container, rows=SxExpr(rows_html),
|
||||
pre_action=pre_action, add_url=add_url)
|
||||
return sx_call("events-slots-table-from-data",
|
||||
list_container=list_container, slots=slots_data or None,
|
||||
pre_action=pre_action,
|
||||
add_url=url_for("calendar.slots.add_form", calendar_slug=cal_slug),
|
||||
tr_cls=tr_cls, pill_cls=pill_cls, action_btn=action_btn,
|
||||
hx_select=hx_select,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -279,7 +250,7 @@ def render_slots_table(slots, calendar) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_slot_edit_form(slot, calendar) -> str:
|
||||
"""Render slot edit form (replaces _types/slot/_edit.html)."""
|
||||
"""Render slot edit form via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -291,38 +262,25 @@ def render_slot_edit_form(slot, calendar) -> str:
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
sid = slot.id
|
||||
|
||||
put_url = url_for("calendar.slots.slot.put", calendar_slug=cal_slug, slot_id=sid)
|
||||
cancel_url = url_for("calendar.slots.slot.get_view", calendar_slug=cal_slug, slot_id=sid)
|
||||
|
||||
cost = getattr(slot, "cost", None)
|
||||
cost_val = f"{cost:.2f}" if cost is not None else ""
|
||||
start_val = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
end_val = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
desc_val = getattr(slot, "description", "") or ""
|
||||
|
||||
# Days checkboxes
|
||||
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
||||
("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
|
||||
days_data = [{"name": k, "label": lbl, "checked": getattr(slot, k, False) or None}
|
||||
for k, lbl in day_keys]
|
||||
all_checked = all(getattr(slot, k, False) for k, _ in day_keys)
|
||||
|
||||
days_parts = [sx_call("events-day-all-checkbox",
|
||||
checked="checked" if all_checked else None)]
|
||||
for key, label in day_keys:
|
||||
checked = getattr(slot, key, False)
|
||||
days_parts.append(sx_call("events-day-checkbox",
|
||||
name=key, label=label,
|
||||
checked="checked" if checked else None))
|
||||
days_html = "".join(days_parts)
|
||||
|
||||
flexible = getattr(slot, "flexible", False)
|
||||
|
||||
return sx_call("events-slot-edit-form",
|
||||
return sx_call("events-slot-edit-form-from-data",
|
||||
slot_id=str(sid), list_container=list_container,
|
||||
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
|
||||
name_val=slot.name or "", cost_val=cost_val,
|
||||
start_val=start_val, end_val=end_val,
|
||||
desc_val=desc_val, days=SxExpr(days_html),
|
||||
flexible_checked="checked" if flexible else None,
|
||||
put_url=url_for("calendar.slots.slot.put", calendar_slug=cal_slug, slot_id=sid),
|
||||
cancel_url=url_for("calendar.slots.slot.get_view", calendar_slug=cal_slug, slot_id=sid),
|
||||
csrf=csrf,
|
||||
name_val=slot.name or "",
|
||||
cost_val=f"{cost:.2f}" if cost is not None else "",
|
||||
start_val=slot.time_start.strftime("%H:%M") if slot.time_start else "",
|
||||
end_val=slot.time_end.strftime("%H:%M") if slot.time_end else "",
|
||||
desc_val=getattr(slot, "description", "") or "",
|
||||
days_data=days_data, all_checked=all_checked or None,
|
||||
flexible_checked="checked" if getattr(slot, "flexible", False) else None,
|
||||
action_btn=action_btn, cancel_btn=cancel_btn)
|
||||
|
||||
|
||||
@@ -331,7 +289,7 @@ def render_slot_edit_form(slot, calendar) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_slot_add_form(calendar) -> str:
|
||||
"""Render slot add form (replaces _types/slots/_add.html)."""
|
||||
"""Render slot add form via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -341,23 +299,16 @@ def render_slot_add_form(calendar) -> str:
|
||||
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
|
||||
cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
|
||||
# Days checkboxes (all unchecked for add)
|
||||
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
||||
("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
|
||||
days_parts = [sx_call("events-day-all-checkbox", checked=None)]
|
||||
for key, label in day_keys:
|
||||
days_parts.append(sx_call("events-day-checkbox", name=key, label=label, checked=None))
|
||||
days_html = "".join(days_parts)
|
||||
days_data = [{"name": k, "label": lbl} for k, lbl in day_keys]
|
||||
|
||||
return sx_call("events-slot-add-form",
|
||||
post_url=post_url, csrf=csrf_hdr,
|
||||
days=SxExpr(days_html),
|
||||
return sx_call("events-slot-add-form-from-data",
|
||||
post_url=url_for("calendar.slots.post", calendar_slug=cal_slug),
|
||||
csrf=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
days_data=days_data,
|
||||
action_btn=action_btn, cancel_btn=cancel_btn,
|
||||
cancel_url=cancel_url)
|
||||
cancel_url=url_for("calendar.slots.add_button", calendar_slug=cal_slug))
|
||||
|
||||
|
||||
def render_slot_add_button(calendar) -> str:
|
||||
|
||||
@@ -6,49 +6,25 @@ from markupsafe import escape
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
from .utils import (
|
||||
_ticket_state_badge_html, _list_container, _cart_icon_oob,
|
||||
)
|
||||
from .utils import _list_container, _cart_icon_ctx
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket widget (inline +/- for entry cards)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str:
|
||||
"""Render the inline +/- ticket widget."""
|
||||
csrf_token_val = ""
|
||||
if ctx:
|
||||
ct = ctx.get("csrf_token")
|
||||
csrf_token_val = ct() if callable(ct) else (ct or "")
|
||||
else:
|
||||
try:
|
||||
from flask_wtf.csrf import generate_csrf
|
||||
csrf_token_val = generate_csrf()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _ticket_widget_data(entry, qty: int, ticket_url: str) -> dict:
|
||||
"""Extract ticket widget data for sx composition."""
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
eid = entry.id
|
||||
tp = getattr(entry, "ticket_price", 0) or 0
|
||||
tgt = f"#page-ticket-{eid}"
|
||||
|
||||
def _tw_form(count_val, btn_html):
|
||||
return sx_call("events-tw-form",
|
||||
ticket_url=ticket_url, target=tgt,
|
||||
csrf=csrf_token_val, entry_id=str(eid),
|
||||
count_val=str(count_val), btn=btn_html)
|
||||
|
||||
if qty == 0:
|
||||
inner = _tw_form(1, sx_call("events-tw-cart-plus"))
|
||||
else:
|
||||
minus = _tw_form(qty - 1, sx_call("events-tw-minus"))
|
||||
cart_icon = sx_call("events-tw-cart-icon", qty=str(qty))
|
||||
plus = _tw_form(qty + 1, sx_call("events-tw-plus"))
|
||||
inner = minus + cart_icon + plus
|
||||
|
||||
return sx_call("events-tw-widget",
|
||||
entry_id=str(eid), price=f"\u00a3{tp:.2f}",
|
||||
inner=SxExpr(inner))
|
||||
return {
|
||||
"entry_id": str(eid),
|
||||
"price": f"\u00a3{tp:.2f}",
|
||||
"qty": qty,
|
||||
"ticket_url": ticket_url,
|
||||
"csrf": generate_csrf_token(),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -56,37 +32,33 @@ def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
||||
"""Render my tickets list."""
|
||||
"""Render my tickets list via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
|
||||
ticket_cards = []
|
||||
ticket_data = []
|
||||
if tickets:
|
||||
for ticket in tickets:
|
||||
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||
entry = getattr(ticket, "entry", None)
|
||||
entry_name = entry.name if entry else "Unknown event"
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
|
||||
time_str = ""
|
||||
if entry and entry.start_at:
|
||||
time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
ticket_data.append({
|
||||
"href": url_for("defpage_ticket_detail", code=ticket.code),
|
||||
"entry-name": entry.name if entry else "Unknown event",
|
||||
"type-name": tt.name if tt else None,
|
||||
"time-str": time_str or None,
|
||||
"cal-name": cal.name if cal else None,
|
||||
"state": getattr(ticket, "state", ""),
|
||||
"code-prefix": ticket.code[:8],
|
||||
})
|
||||
|
||||
ticket_cards.append(sx_call("events-ticket-card",
|
||||
href=href, entry_name=entry_name,
|
||||
type_name=tt.name if tt else None,
|
||||
time_str=time_str or None,
|
||||
cal_name=cal.name if cal else None,
|
||||
badge=_ticket_state_badge_html(state),
|
||||
code_prefix=ticket.code[:8]))
|
||||
|
||||
cards_html = "".join(ticket_cards)
|
||||
return sx_call("events-tickets-panel",
|
||||
return sx_call("events-tickets-panel-from-data",
|
||||
list_container=_list_container(ctx),
|
||||
has_tickets=bool(tickets), cards=SxExpr(cards_html))
|
||||
tickets=ticket_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -94,7 +66,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
||||
"""Render a single ticket detail with QR code."""
|
||||
"""Render a single ticket detail with QR code via data + sx defcomp."""
|
||||
from quart import url_for
|
||||
|
||||
entry = getattr(ticket, "entry", None)
|
||||
@@ -105,22 +77,11 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
|
||||
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
||||
header_bg = bg_map.get(state, "bg-stone-50")
|
||||
entry_name = entry.name if entry else "Ticket"
|
||||
back_href = url_for("defpage_my_tickets")
|
||||
|
||||
# Badge with larger sizing
|
||||
badge = (_ticket_state_badge_html(state)).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
||||
|
||||
# Time info
|
||||
time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None
|
||||
time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None
|
||||
if time_range and entry.end_at:
|
||||
time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
|
||||
tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None
|
||||
checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None
|
||||
|
||||
qr_script = (
|
||||
f"(function(){{var c=document.getElementById('ticket-qr-{code}');"
|
||||
"if(c&&typeof QRCode!=='undefined'){"
|
||||
@@ -129,13 +90,16 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
||||
"}})()"
|
||||
)
|
||||
|
||||
return sx_call("events-ticket-detail",
|
||||
list_container=_list_container(ctx), back_href=back_href,
|
||||
header_bg=header_bg, entry_name=entry_name,
|
||||
badge=SxExpr(badge), type_name=tt.name if tt else None,
|
||||
return sx_call("events-ticket-detail-from-data",
|
||||
list_container=_list_container(ctx),
|
||||
back_href=url_for("defpage_my_tickets"),
|
||||
header_bg=bg_map.get(state, "bg-stone-50"),
|
||||
entry_name=entry.name if entry else "Ticket",
|
||||
state=state, type_name=tt.name if tt else None,
|
||||
code=code, time_date=time_date, time_range=time_range,
|
||||
cal_name=cal.name if cal else None,
|
||||
type_desc=tt_desc, checkin_str=checkin_str,
|
||||
type_desc=f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None,
|
||||
checkin_str=checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None,
|
||||
qr_script=qr_script)
|
||||
|
||||
|
||||
@@ -144,62 +108,38 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str:
|
||||
"""Render ticket admin dashboard."""
|
||||
"""Render ticket admin dashboard via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
lookup_url = url_for("ticket_admin.lookup")
|
||||
|
||||
# Stats cards
|
||||
stats_html = ""
|
||||
for label, key, border, bg, text_cls in [
|
||||
("Total", "total", "border-stone-200", "", "text-stone-900"),
|
||||
("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"),
|
||||
("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"),
|
||||
("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"),
|
||||
]:
|
||||
val = stats.get(key, 0)
|
||||
lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500"
|
||||
stats_html += sx_call("events-ticket-admin-stat",
|
||||
border=border, bg=bg, text_cls=text_cls,
|
||||
label_cls=lbl_cls, value=str(val), label=label)
|
||||
|
||||
# Ticket rows
|
||||
rows_html = ""
|
||||
ticket_data = []
|
||||
for ticket in tickets:
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
ticket_data.append({
|
||||
"code": code,
|
||||
"code-short": code[:12] + "...",
|
||||
"entry-name": entry.name if entry else "\u2014",
|
||||
"date-str": entry.start_at.strftime("%d %b %Y, %H:%M") if entry and entry.start_at else None,
|
||||
"type-name": tt.name if tt else "\u2014",
|
||||
"state": state,
|
||||
"checkin-url": url_for("ticket_admin.do_checkin", code=code) if state in ("confirmed", "reserved") else None,
|
||||
"csrf": csrf,
|
||||
"checked-in-time": checked_in_at.strftime("%H:%M") if checked_in_at else None,
|
||||
})
|
||||
|
||||
date_html = ""
|
||||
if entry and entry.start_at:
|
||||
date_html = sx_call("events-ticket-admin-date",
|
||||
date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))
|
||||
|
||||
action_html = ""
|
||||
if state in ("confirmed", "reserved"):
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
||||
action_html = sx_call("events-ticket-admin-checkin-form",
|
||||
checkin_url=checkin_url, code=code, csrf=csrf)
|
||||
elif state == "checked_in":
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
|
||||
action_html = sx_call("events-ticket-admin-checked-in",
|
||||
time_str=t_str)
|
||||
|
||||
rows_html += sx_call("events-ticket-admin-row",
|
||||
code=code, code_short=code[:12] + "...",
|
||||
entry_name=entry.name if entry else "\u2014",
|
||||
date=SxExpr(date_html),
|
||||
type_name=tt.name if tt else "\u2014",
|
||||
badge=_ticket_state_badge_html(state),
|
||||
action=SxExpr(action_html))
|
||||
|
||||
return sx_call("events-ticket-admin-panel",
|
||||
list_container=_list_container(ctx), stats=SxExpr(stats_html),
|
||||
lookup_url=lookup_url, has_tickets=bool(tickets),
|
||||
rows=SxExpr(rows_html))
|
||||
return sx_call("events-ticket-admin-panel-from-data",
|
||||
list_container=_list_container(ctx),
|
||||
lookup_url=url_for("ticket_admin.lookup"),
|
||||
tickets=ticket_data or None,
|
||||
total=stats.get("total", 0),
|
||||
confirmed=stats.get("confirmed", 0),
|
||||
checked_in=stats.get("checked_in", 0),
|
||||
reserved=stats.get("reserved", 0))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -208,7 +148,8 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str:
|
||||
|
||||
def render_ticket_widget(entry, qty: int, ticket_url: str) -> str:
|
||||
"""Render the +/- ticket widget for page_summary / all_events adjust_ticket."""
|
||||
return _ticket_widget_html(entry, qty, ticket_url, ctx={})
|
||||
data = _ticket_widget_data(entry, qty, ticket_url)
|
||||
return sx_call("events-tw-widget-from-data", **data)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -226,20 +167,13 @@ def render_checkin_result(success: bool, error: str | None, ticket) -> str:
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
time_str = checked_in_at.strftime("%H:%M") if checked_in_at else "Just now"
|
||||
|
||||
date_html = ""
|
||||
if entry and entry.start_at:
|
||||
date_html = sx_call("events-ticket-admin-date",
|
||||
date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))
|
||||
|
||||
return sx_call("events-checkin-success-row",
|
||||
return sx_call("events-checkin-success-row-from-data",
|
||||
code=code, code_short=code[:12] + "...",
|
||||
entry_name=entry.name if entry else "\u2014",
|
||||
date=SxExpr(date_html),
|
||||
date_str=entry.start_at.strftime("%d %b %Y, %H:%M") if entry and entry.start_at else None,
|
||||
type_name=tt.name if tt else "\u2014",
|
||||
badge=_ticket_state_badge_html("checked_in"),
|
||||
time_str=time_str)
|
||||
time_str=checked_in_at.strftime("%H:%M") if checked_in_at else "Just now")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -247,7 +181,7 @@ def render_checkin_result(success: bool, error: str | None, ticket) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_lookup_result(ticket, error: str | None) -> str:
|
||||
"""Render ticket lookup result: error div or ticket info card."""
|
||||
"""Render ticket lookup result via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
@@ -261,38 +195,21 @@ def render_lookup_result(ticket, error: str | None) -> str:
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
# Info section
|
||||
info_html = sx_call("events-lookup-info",
|
||||
entry_name=entry.name if entry else "Unknown event")
|
||||
if tt:
|
||||
info_html += sx_call("events-lookup-type", type_name=tt.name)
|
||||
if entry and entry.start_at:
|
||||
info_html += sx_call("events-lookup-date",
|
||||
date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M"))
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
if cal:
|
||||
info_html += sx_call("events-lookup-cal", cal_name=cal.name)
|
||||
info_html += sx_call("events-lookup-status",
|
||||
badge=_ticket_state_badge_html(state), code=code)
|
||||
if checked_in_at:
|
||||
info_html += sx_call("events-lookup-checkin-time",
|
||||
date_str=checked_in_at.strftime("%B %d, %Y at %H:%M"))
|
||||
|
||||
# Action area
|
||||
action_html = ""
|
||||
checkin_url = None
|
||||
if state in ("confirmed", "reserved"):
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
||||
action_html = sx_call("events-lookup-checkin-btn",
|
||||
checkin_url=checkin_url, code=code, csrf=csrf)
|
||||
elif state == "checked_in":
|
||||
action_html = sx_call("events-lookup-checked-in")
|
||||
elif state == "cancelled":
|
||||
action_html = sx_call("events-lookup-cancelled")
|
||||
|
||||
return sx_call("events-lookup-card",
|
||||
info=SxExpr(info_html), code=code, action=SxExpr(action_html))
|
||||
return sx_call("events-lookup-result-from-data",
|
||||
entry_name=entry.name if entry else "Unknown event",
|
||||
type_name=tt.name if tt else None,
|
||||
date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M") if entry and entry.start_at else None,
|
||||
cal_name=cal.name if cal else None,
|
||||
state=state, code=code,
|
||||
checked_in_str=checked_in_at.strftime("%B %d, %Y at %H:%M") if checked_in_at else None,
|
||||
checkin_url=checkin_url,
|
||||
csrf=generate_csrf_token())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -300,7 +217,7 @@ def render_lookup_result(ticket, error: str | None) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_entry_tickets_admin(entry, tickets: list) -> str:
|
||||
"""Render admin ticket table for a specific entry."""
|
||||
"""Render admin ticket table via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -308,39 +225,29 @@ def render_entry_tickets_admin(entry, tickets: list) -> str:
|
||||
count = len(tickets)
|
||||
suffix = "s" if count != 1 else ""
|
||||
|
||||
rows_html = ""
|
||||
ticket_data = []
|
||||
for ticket in tickets:
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
|
||||
action_html = ""
|
||||
checkin_url = None
|
||||
if state in ("confirmed", "reserved"):
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
||||
action_html = sx_call("events-entry-tickets-admin-checkin",
|
||||
checkin_url=checkin_url, code=code, csrf=csrf)
|
||||
elif state == "checked_in":
|
||||
t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
|
||||
action_html = sx_call("events-ticket-admin-checked-in",
|
||||
time_str=t_str)
|
||||
ticket_data.append({
|
||||
"code": code,
|
||||
"code-short": code[:12] + "...",
|
||||
"type-name": tt.name if tt else "\u2014",
|
||||
"state": state,
|
||||
"checkin-url": checkin_url,
|
||||
"checked-in-time": checked_in_at.strftime("%H:%M") if checked_in_at else None,
|
||||
})
|
||||
|
||||
rows_html += sx_call("events-entry-tickets-admin-row",
|
||||
code=code, code_short=code[:12] + "...",
|
||||
type_name=tt.name if tt else "\u2014",
|
||||
badge=_ticket_state_badge_html(state),
|
||||
action=SxExpr(action_html))
|
||||
|
||||
if tickets:
|
||||
body_html = sx_call("events-entry-tickets-admin-table",
|
||||
rows=SxExpr(rows_html))
|
||||
else:
|
||||
body_html = sx_call("events-entry-tickets-admin-empty")
|
||||
|
||||
return sx_call("events-entry-tickets-admin-panel",
|
||||
return sx_call("events-entry-tickets-admin-from-data",
|
||||
entry_name=entry.name,
|
||||
count_label=f"{count} ticket{suffix}",
|
||||
body=body_html)
|
||||
tickets=ticket_data or None,
|
||||
csrf=csrf)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -383,7 +290,7 @@ def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str:
|
||||
"""Render ticket types table with rows and add button."""
|
||||
"""Render ticket types table via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -397,40 +304,38 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
|
||||
rows_html = ""
|
||||
types_data = []
|
||||
if ticket_types:
|
||||
for tt in ticket_types:
|
||||
tt_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
)
|
||||
del_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
)
|
||||
cost = getattr(tt, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
|
||||
rows_html += sx_call("events-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=str(tt.count), action_btn=action_btn,
|
||||
del_url=del_url,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
else:
|
||||
rows_html = sx_call("events-ticket-types-empty-row")
|
||||
types_data.append({
|
||||
"tt-href": url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
),
|
||||
"tt-name": tt.name,
|
||||
"cost-str": f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00",
|
||||
"count": str(tt.count),
|
||||
"del-url": url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
),
|
||||
})
|
||||
|
||||
add_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
|
||||
calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
|
||||
)
|
||||
|
||||
return sx_call("events-ticket-types-table",
|
||||
list_container=list_container, rows=SxExpr(rows_html),
|
||||
action_btn=action_btn, add_url=add_url)
|
||||
return sx_call("events-ticket-types-table-from-data",
|
||||
list_container=list_container,
|
||||
ticket_types=types_data or None,
|
||||
action_btn=action_btn, add_url=add_url,
|
||||
tr_cls=tr_cls, pill_cls=pill_cls,
|
||||
hx_select=hx_select,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -438,34 +343,22 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
||||
"""Render buy result card with created tickets + OOB cart icon."""
|
||||
"""Render buy result card with OOB cart icon — single response component."""
|
||||
from quart import url_for
|
||||
|
||||
cart_html = _cart_icon_oob(cart_count)
|
||||
tickets = [
|
||||
{"href": url_for("defpage_ticket_detail", code=t.code),
|
||||
"code_short": t.code[:12] + "..."}
|
||||
for t in created_tickets
|
||||
]
|
||||
cart_ctx = _cart_icon_ctx(cart_count)
|
||||
|
||||
count = len(created_tickets)
|
||||
suffix = "s" if count != 1 else ""
|
||||
|
||||
tickets_html = ""
|
||||
for ticket in created_tickets:
|
||||
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||
tickets_html += sx_call("events-buy-result-ticket",
|
||||
href=href, code_short=ticket.code[:12] + "...")
|
||||
|
||||
remaining_html = ""
|
||||
if remaining is not None:
|
||||
r_suffix = "s" if remaining != 1 else ""
|
||||
remaining_html = sx_call("events-buy-result-remaining",
|
||||
text=f"{remaining} ticket{r_suffix} remaining")
|
||||
|
||||
my_href = url_for("defpage_my_tickets")
|
||||
|
||||
return cart_html + sx_call("events-buy-result",
|
||||
entry_id=str(entry.id),
|
||||
count_label=f"{count} ticket{suffix} reserved",
|
||||
tickets=SxExpr(tickets_html),
|
||||
remaining=SxExpr(remaining_html),
|
||||
my_tickets_href=my_href)
|
||||
return sx_call("events-buy-response",
|
||||
entry_id=str(entry.id),
|
||||
tickets=tickets,
|
||||
remaining=remaining,
|
||||
my_tickets_href=url_for("defpage_my_tickets"),
|
||||
**cart_ctx)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -474,90 +367,41 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
||||
|
||||
def render_buy_form(entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type) -> str:
|
||||
"""Render the ticket buy/adjust form with +/- controls."""
|
||||
"""Render the ticket buy/adjust form — data only, .sx does layout."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
eid = entry.id
|
||||
eid_s = str(eid)
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
state = getattr(entry, "state", "")
|
||||
ticket_types = getattr(entry, "ticket_types", None) or []
|
||||
|
||||
if tp is None:
|
||||
return ""
|
||||
|
||||
if state != "confirmed":
|
||||
return sx_call("events-buy-not-confirmed", entry_id=eid_s)
|
||||
ticket_types_orm = getattr(entry, "ticket_types", None) or []
|
||||
active_types = [tt for tt in ticket_types_orm if getattr(tt, "deleted_at", None) is None]
|
||||
|
||||
adjust_url = url_for("tickets.adjust_quantity")
|
||||
target = f"#ticket-buy-{eid}"
|
||||
types_data = [
|
||||
{"id": tt.id, "name": tt.name,
|
||||
"cost_str": f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"}
|
||||
for tt in active_types
|
||||
]
|
||||
|
||||
# Info line
|
||||
info_html = ""
|
||||
info_items = ""
|
||||
if ticket_sold_count:
|
||||
info_items += sx_call("events-buy-info-sold",
|
||||
count=str(ticket_sold_count))
|
||||
if ticket_remaining is not None:
|
||||
info_items += sx_call("events-buy-info-remaining",
|
||||
count=str(ticket_remaining))
|
||||
if user_ticket_count:
|
||||
info_items += sx_call("events-buy-info-basket",
|
||||
count=str(user_ticket_count))
|
||||
if info_items:
|
||||
info_html = sx_call("events-buy-info-bar", items=SxExpr(info_items))
|
||||
# String keys so .sx can look up via (get counts (str id))
|
||||
counts_by_type = {}
|
||||
if user_ticket_counts_by_type:
|
||||
counts_by_type = {str(k): v for k, v in user_ticket_counts_by_type.items()}
|
||||
|
||||
active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None]
|
||||
|
||||
body_html = ""
|
||||
if active_types:
|
||||
type_items = ""
|
||||
for tt in active_types:
|
||||
type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0
|
||||
cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"
|
||||
type_items += sx_call("events-buy-type-item",
|
||||
type_name=tt.name, cost_str=cost_str,
|
||||
adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id))
|
||||
body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items))
|
||||
else:
|
||||
qty = user_ticket_count or 0
|
||||
body_html = sx_call("events-buy-default",
|
||||
price_str=f"\u00a3{tp:.2f}",
|
||||
adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, qty))
|
||||
|
||||
return sx_call("events-buy-panel",
|
||||
entry_id=eid_s, info=SxExpr(info_html), body=body_html)
|
||||
|
||||
|
||||
def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None):
|
||||
"""Render +/- ticket controls for buy form."""
|
||||
from quart import url_for
|
||||
|
||||
tt_html = sx_call("events-adjust-tt-hidden",
|
||||
ticket_type_id=str(ticket_type_id)) if ticket_type_id else ""
|
||||
eid_s = str(entry_id)
|
||||
|
||||
def _adj_form(count_val, btn_html, *, extra_cls=""):
|
||||
return sx_call("events-adjust-form",
|
||||
adjust_url=adjust_url, target=target,
|
||||
extra_cls=extra_cls, csrf=csrf,
|
||||
entry_id=eid_s, tt=tt_html or None,
|
||||
count_val=str(count_val), btn=btn_html)
|
||||
|
||||
if count == 0:
|
||||
return _adj_form(1, sx_call("events-adjust-cart-plus"),
|
||||
extra_cls="flex items-center")
|
||||
|
||||
my_tickets_href = url_for("defpage_my_tickets")
|
||||
minus = _adj_form(count - 1, sx_call("events-adjust-minus"))
|
||||
cart_icon = sx_call("events-adjust-cart-icon",
|
||||
href=my_tickets_href, count=str(count))
|
||||
plus = _adj_form(count + 1, sx_call("events-adjust-plus"))
|
||||
|
||||
return sx_call("events-adjust-controls",
|
||||
minus=minus, cart_icon=cart_icon, plus=plus)
|
||||
return sx_call("events-buy-form",
|
||||
entry_id=entry.id,
|
||||
state=getattr(entry, "state", ""),
|
||||
price_str=f"\u00a3{tp:.2f}",
|
||||
adjust_url=url_for("tickets.adjust_quantity"),
|
||||
csrf=generate_csrf_token(),
|
||||
my_tickets_href=url_for("defpage_my_tickets"),
|
||||
info_sold=ticket_sold_count or None,
|
||||
info_remaining=ticket_remaining,
|
||||
info_basket=user_ticket_count or None,
|
||||
ticket_types=types_data if types_data else None,
|
||||
user_ticket_counts_by_type=counts_by_type if counts_by_type else None,
|
||||
user_ticket_count=user_ticket_count or 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -567,13 +411,44 @@ def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket
|
||||
def render_adjust_response(entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type,
|
||||
cart_count) -> str:
|
||||
"""Render ticket adjust response: OOB cart icon + buy form."""
|
||||
cart_html = _cart_icon_oob(cart_count)
|
||||
form_html = render_buy_form(
|
||||
entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type,
|
||||
)
|
||||
return cart_html + form_html
|
||||
"""Render ticket adjust response — single response component with OOB cart."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
if tp is None:
|
||||
return ""
|
||||
|
||||
ticket_types_orm = getattr(entry, "ticket_types", None) or []
|
||||
active_types = [tt for tt in ticket_types_orm if getattr(tt, "deleted_at", None) is None]
|
||||
|
||||
types_data = [
|
||||
{"id": tt.id, "name": tt.name,
|
||||
"cost_str": f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"}
|
||||
for tt in active_types
|
||||
]
|
||||
|
||||
# String keys so .sx can look up via (get counts (str id))
|
||||
counts_by_type = {}
|
||||
if user_ticket_counts_by_type:
|
||||
counts_by_type = {str(k): v for k, v in user_ticket_counts_by_type.items()}
|
||||
|
||||
cart_ctx = _cart_icon_ctx(cart_count)
|
||||
|
||||
return sx_call("events-adjust-response",
|
||||
entry_id=entry.id,
|
||||
state=getattr(entry, "state", ""),
|
||||
price_str=f"\u00a3{tp:.2f}",
|
||||
adjust_url=url_for("tickets.adjust_quantity"),
|
||||
csrf=generate_csrf_token(),
|
||||
my_tickets_href=url_for("defpage_my_tickets"),
|
||||
info_sold=ticket_sold_count or None,
|
||||
info_remaining=ticket_remaining,
|
||||
info_basket=user_ticket_count or None,
|
||||
ticket_types=types_data if types_data else None,
|
||||
user_ticket_counts_by_type=counts_by_type if counts_by_type else None,
|
||||
user_ticket_count=user_ticket_count or 0,
|
||||
**cart_ctx)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -14,31 +14,6 @@ from shared.sx.helpers import sx_call
|
||||
# Post header helpers — thin wrapper over shared post_header_sx
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _clear_oob(*ids: str) -> str:
|
||||
"""Generate OOB swaps to remove orphaned header rows/children."""
|
||||
from shared.sx.helpers import sx_call
|
||||
return "".join(sx_call("clear-oob-div", id=i) for i in ids)
|
||||
|
||||
|
||||
# All possible header row/child IDs at each depth (deepest first)
|
||||
_EVENTS_DEEP_IDS = [
|
||||
"entry-admin-row", "entry-admin-header-child",
|
||||
"entry-row", "entry-header-child",
|
||||
"day-admin-row", "day-admin-header-child",
|
||||
"day-row", "day-header-child",
|
||||
"calendar-admin-row", "calendar-admin-header-child",
|
||||
"calendar-row", "calendar-header-child",
|
||||
"calendars-row", "calendars-header-child",
|
||||
"post-admin-row", "post-admin-header-child",
|
||||
]
|
||||
|
||||
|
||||
def _clear_deeper_oob(*keep_ids: str) -> str:
|
||||
"""Clear all events header rows/children NOT in keep_ids."""
|
||||
to_clear = [i for i in _EVENTS_DEEP_IDS if i not in keep_ids]
|
||||
return _clear_oob(*to_clear)
|
||||
|
||||
|
||||
async def _ensure_container_nav(ctx: dict) -> dict:
|
||||
"""Fetch container_nav if not already present (for post header row)."""
|
||||
if ctx.get("container_nav"):
|
||||
@@ -87,19 +62,6 @@ def _entry_state_badge_html(state: str) -> str:
|
||||
return sx_call("badge", cls=cls, label=label)
|
||||
|
||||
|
||||
def _ticket_state_badge_html(state: str) -> str:
|
||||
"""Render a ticket state badge."""
|
||||
cls_map = {
|
||||
"confirmed": "bg-emerald-100 text-emerald-800",
|
||||
"checked_in": "bg-blue-100 text-blue-800",
|
||||
"reserved": "bg-amber-100 text-amber-800",
|
||||
"cancelled": "bg-red-100 text-red-800",
|
||||
}
|
||||
cls = cls_map.get(state, "bg-stone-100 text-stone-700")
|
||||
label = (state or "").replace("_", " ").capitalize()
|
||||
return sx_call("badge", cls=cls, label=label)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# View toggle + SVG caching
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -150,6 +112,12 @@ def _view_toggle_html(ctx: dict, view: str) -> str:
|
||||
|
||||
def _cart_icon_oob(count: int) -> str:
|
||||
"""Render the OOB cart icon/badge swap."""
|
||||
ctx = _cart_icon_ctx(count)
|
||||
return sx_call("events-cart-icon", **ctx)
|
||||
|
||||
|
||||
def _cart_icon_ctx(count: int) -> dict:
|
||||
"""Return data dict for the ~events-cart-icon component."""
|
||||
from quart import g
|
||||
|
||||
blog_url_fn = getattr(g, "blog_url", None)
|
||||
@@ -160,11 +128,11 @@ def _cart_icon_oob(count: int) -> str:
|
||||
site_obj = site_fn() if callable(site_fn) else site_fn
|
||||
logo = getattr(site_obj, "logo", "") if site_obj else ""
|
||||
|
||||
if count == 0:
|
||||
blog_href = blog_url_fn("/") if blog_url_fn else "/"
|
||||
return sx_call("events-cart-icon-logo",
|
||||
blog_href=blog_href, logo=logo)
|
||||
|
||||
blog_href = blog_url_fn("/") if blog_url_fn else "/"
|
||||
cart_href = cart_url_fn("/") if cart_url_fn else "/"
|
||||
return sx_call("events-cart-icon-badge",
|
||||
cart_href=cart_href, count=str(count))
|
||||
return {
|
||||
"cart_count": count,
|
||||
"blog_href": blog_href,
|
||||
"cart_href": cart_href,
|
||||
"logo": logo,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user