Refactor SX templates: shared components, Python migration, cleanup

- Extract shared components (empty-state, delete-btn, sentinel, crud-*,
  view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth
  forms, order tables/detail/checkout)
- Migrate all Python sx_call() callers to use shared components directly
- Remove 55+ thin wrapper defcomps from domain .sx files
- Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc)
- Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx
- Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx
- Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0)
- Add SX response validation and debug headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:34:34 +00:00
parent 755313bd29
commit c0d369eb8e
58 changed files with 3473 additions and 1210 deletions

View File

@@ -50,9 +50,11 @@ def register():
page = int(request.args.get("page", 1))
exclude = request.args.get("exclude", "")
excludes = [e.strip() for e in exclude.split(",") if e.strip()]
current_calendar = request.args.get("current_calendar", "")
styles = current_app.jinja_env.globals.get("styles", {})
nav_class = styles.get("nav_button_less_pad", "")
nav_class = styles.get("nav_button", "")
select_colours = current_app.jinja_env.globals.get("select_colours", "")
parts = []
# Calendar entries nav
@@ -87,8 +89,10 @@ def register():
)
for cal in calendars:
href = events_url(f"/{post_slug}/{cal.slug}/")
is_selected = (cal.slug == current_calendar) if current_calendar else False
parts.append(sx_call("calendar-link-nav",
href=href, name=cal.name, nav_class=nav_class))
href=href, name=cal.name, nav_class=nav_class,
is_selected=is_selected, select_colours=select_colours))
if not parts:
return ""

View File

@@ -36,45 +36,6 @@
(div :class "hidden sm:grid grid-cols-7 text-center text-md font-semibold text-stone-700 mb-2" weekdays)
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells))))
(defcomp ~events-calendars-create-form (&key create-url csrf)
(<>
(div :id "cal-create-errors" :class "mt-2 text-sm text-red-600")
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
:sx-target "#calendars-list" :sx-select "#calendars-list" :sx-swap "outerHTML"
:sx-on:beforeRequest "document.querySelector('#cal-create-errors').textContent='';"
:sx-on:responseError "document.querySelector('#cal-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
(input :type "hidden" :name "csrf_token" :value csrf)
(div :class "flex-1"
(label :class "block text-sm text-gray-600" "Name")
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
:placeholder "e.g. Events, Gigs, Meetings"))
(button :type "submit" :class "border rounded px-3 py-2" "Add calendar"))))
(defcomp ~events-calendars-panel (&key form list)
(section :class "p-4"
form
(div :id "calendars-list" :class "mt-6" list)))
(defcomp ~events-calendars-empty ()
(p :class "text-gray-500 mt-4" "No calendars yet. Create one above."))
(defcomp ~events-calendars-item (&key href cal-name cal-slug del-url csrf-hdr)
(div :class "mt-6 border rounded-lg p-4"
(div :class "flex items-center justify-between gap-3"
(a :class "flex items-baseline gap-3" :href href
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true"
(h3 :class "font-semibold" cal-name)
(h4 :class "text-gray-500" (str "/" cal-slug "/")))
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
:data-confirm true :data-confirm-title "Delete calendar?"
:data-confirm-text "Entries will be hidden (soft delete)"
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
:sx-delete del-url :sx-trigger "confirmed"
:sx-target "#calendars-list" :sx-select "#calendars-list" :sx-swap "outerHTML"
:sx-headers csrf-hdr
(i :class "fa-solid fa-trash")))))
(defcomp ~events-calendar-description-display (&key description edit-url)
(div :id "calendar-description"
(if description

View File

@@ -1,8 +1,5 @@
;; Events entry card components (all events / page summary)
(defcomp ~events-state-badge (&key cls label)
(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " cls) label))
(defcomp ~events-entry-title-linked (&key href name)
(a :href href :class "hover:text-emerald-700"
(h2 :class "text-lg font-semibold text-stone-900" name)))
@@ -61,43 +58,8 @@
(div :class "pt-2 pb-1"
(h3 :class "text-sm font-semibold text-stone-500 uppercase tracking-wide" date-str)))
(defcomp ~events-sentinel (&key page next-url)
(div :id (str "sentinel-" page) :class "h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
:role "status" :aria-hidden "true"
(div :class "text-center text-xs text-stone-400" "loading...")))
(defcomp ~events-list-svg ()
(svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none"
:viewBox "0 0 24 24" :stroke "currentColor" :stroke-width "2"
(path :stroke-linecap "round" :stroke-linejoin "round" :d "M4 6h16M4 12h16M4 18h16")))
(defcomp ~events-tile-svg ()
(svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none"
:viewBox "0 0 24 24" :stroke "currentColor" :stroke-width "2"
(path :stroke-linecap "round" :stroke-linejoin "round"
:d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z")))
(defcomp ~events-view-toggle (&key list-href tile-href hx-select list-active tile-active list-svg tile-svg)
(div :class "hidden md:flex justify-end px-3 pt-3 gap-1"
(a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true"
:class (str "p-1.5 rounded " list-active) :title "List view"
:_ "on click js localStorage.removeItem('events_view') end"
list-svg)
(a :href tile-href :sx-get tile-href :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true"
:class (str "p-1.5 rounded " tile-active) :title "Tile view"
:_ "on click js localStorage.setItem('events_view','tile') end"
tile-svg)))
(defcomp ~events-grid (&key grid-cls cards)
(div :class grid-cls cards))
(defcomp ~events-empty ()
(div :class "px-3 py-12 text-center text-stone-400"
(i :class "fa fa-calendar-xmark text-4xl mb-3" :aria-hidden "true")
(p :class "text-lg" "No upcoming events")))
(defcomp ~events-main-panel-body (&key toggle body)
(<> toggle body (div :class "pb-8")))

View File

@@ -494,8 +494,4 @@
(defcomp ~events-admin-placeholder-nav ()
(div :class "relative nav-group"
(span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options")))
;; Entry admin main panel — ticket_types link
(defcomp ~events-entry-admin-main-panel (&key link)
link)
(span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options")))

View File

@@ -23,15 +23,6 @@
;; Account page tickets (fragments/account_page_tickets.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-frag-ticket-badge (&key state)
(cond
((= state "checked_in")
(span :class "inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-2.5 py-0.5 text-xs font-medium text-blue-700" "checked in"))
((= state "confirmed")
(span :class "inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700" "confirmed"))
(true
(span :class "inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700" state))))
(defcomp ~events-frag-ticket-item (&key href entry-name date-str calendar-name type-name badge)
(div :class "py-4 first:pt-0 last:pb-0"
(div :class "flex items-start justify-between gap-4"
@@ -50,9 +41,6 @@
(h1 :class "text-xl font-semibold tracking-tight" "Tickets")
items)))
(defcomp ~events-frag-tickets-empty ()
(p :class "text-sm text-stone-500" "No tickets yet."))
(defcomp ~events-frag-tickets-list (&key items)
(div :class "divide-y divide-stone-100" items))
@@ -61,15 +49,6 @@
;; Account page bookings (fragments/account_page_bookings.html)
;; ---------------------------------------------------------------------------
(defcomp ~events-frag-booking-badge (&key state)
(cond
((= state "confirmed")
(span :class "inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700" "confirmed"))
((= state "provisional")
(span :class "inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700" "provisional"))
(true
(span :class "inline-flex items-center rounded-full bg-stone-50 border border-stone-200 px-2.5 py-0.5 text-xs font-medium text-stone-600" state))))
(defcomp ~events-frag-booking-item (&key name date-str calendar-name cost-str badge)
(div :class "py-4 first:pt-0 last:pb-0"
(div :class "flex items-start justify-between gap-4"
@@ -87,8 +66,5 @@
(h1 :class "text-xl font-semibold tracking-tight" "Bookings")
items)))
(defcomp ~events-frag-bookings-empty ()
(p :class "text-sm text-stone-500" "No bookings yet."))
(defcomp ~events-frag-bookings-list (&key items)
(div :class "divide-y divide-stone-100" items))

View File

@@ -1,39 +0,0 @@
;; Events markets components
(defcomp ~events-markets-create-form (&key create-url csrf)
(<>
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
(input :type "hidden" :name "csrf_token" :value csrf)
(div :class "flex-1"
(label :class "block text-sm text-gray-600" "Name")
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
:placeholder "e.g. Farm Shop, Bakery"))
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
(defcomp ~events-markets-panel (&key form list)
(section :class "p-4"
form
(div :id "markets-list" :class "mt-6" list)))
(defcomp ~events-markets-empty ()
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
(defcomp ~events-markets-item (&key href market-name market-slug del-url csrf-hdr)
(div :class "mt-6 border rounded-lg p-4"
(div :class "flex items-center justify-between gap-3"
(a :class "flex items-baseline gap-3" :href href
(h3 :class "font-semibold" market-name)
(h4 :class "text-gray-500" (str "/" market-slug "/")))
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
:data-confirm true :data-confirm-title "Delete market?"
:data-confirm-text "Products will be hidden (soft delete)"
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
:sx-delete del-url :sx-trigger "confirmed"
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
:sx-headers csrf-hdr
(i :class "fa-solid fa-trash")))))

View File

@@ -2,23 +2,9 @@
(defcomp ~events-payments-panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix)
(section :class "p-4 max-w-lg mx-auto"
(div :id "payments-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"
(h3 :class "text-lg font-semibold text-stone-800"
(i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")
(p :class "text-xs text-stone-400" "Configure per-page SumUp credentials. Leave blank to use the global merchant account.")
(form :sx-put update-url :sx-target "#payments-panel" :sx-swap "outerHTML" :sx-select "#payments-panel" :class "space-y-3"
(input :type "hidden" :name "csrf_token" :value csrf)
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Merchant Code")
(input :type "text" :name "merchant_code" :value merchant-code :placeholder "e.g. ME4J6100" :class input-cls))
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "API Key")
(input :type "password" :name "api_key" :value "" :placeholder placeholder :class input-cls)
(when sumup-configured (p :class "text-xs text-stone-400 mt-0.5" "Key is set. Leave blank to keep current key.")))
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Checkout Reference Prefix")
(input :type "text" :name "checkout_prefix" :value checkout-prefix :placeholder "e.g. ROSE-" :class input-cls))
(button :type "submit" :class "px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"
"Save SumUp Settings")
(when sumup-configured (span :class "ml-2 text-xs text-green-600"
(i :class "fa fa-check-circle") " Connected"))))))
(~sumup-settings-form :update-url update-url :csrf csrf :merchant-code merchant-code
:placeholder placeholder :input-cls input-cls :sumup-configured sumup-configured
:checkout-prefix checkout-prefix :sx-select "#payments-panel")))
(defcomp ~events-markets-create-form (&key create-url csrf)
(<>

View File

@@ -37,7 +37,7 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
def _clear_oob(*ids: str) -> str:
"""Generate OOB swaps to remove orphaned header rows/children."""
return "".join(f'<div id="{i}" hx-swap-oob="outerHTML"></div>' for i in ids)
return "".join(f'(div :id "{i}" :hx-swap-oob "outerHTML")' for i in ids)
# All possible header row/child IDs at each depth (deepest first)
@@ -68,11 +68,14 @@ async def _ensure_container_nav(ctx: dict) -> dict:
slug = post.get("slug", "")
if not post_id:
return ctx
from quart import g
from shared.infrastructure.fragments import fetch_fragments
current_cal = getattr(g, "calendar_slug", "") or ""
nav_params = {
"container_type": "page",
"container_id": str(post_id),
"post_slug": slug,
"current_calendar": current_cal,
}
events_nav, market_nav = await fetch_fragments([
("events", "container-nav", nav_params),
@@ -361,12 +364,15 @@ def _calendars_main_panel_sx(ctx: dict) -> str:
form_html = ""
if can_create:
create_url = url_for("calendars.create_calendar")
form_html = sx_call("events-calendars-create-form",
create_url=create_url, csrf=csrf)
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("events-calendars-panel",
form=SxExpr(form_html), list=SxExpr(list_html))
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:
@@ -378,7 +384,8 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
prefix = route_prefix()
if not calendars:
return sx_call("events-calendars-empty")
return sx_call("empty-state", message="No calendars yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
for cal in calendars:
@@ -387,9 +394,12 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
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("events-calendars-item",
href=href, cal_name=cal_name, cal_slug=cal_slug,
del_url=del_url, csrf_hdr=csrf_hdr))
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)
@@ -504,13 +514,15 @@ def _calendar_main_panel_html(ctx: dict) -> str:
bg_cls=bg_cls, name=e.name,
state_label=state_label))
badges_html = "".join(entry_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)))
day_num=SxExpr(day_num_html),
badges=SxExpr(badges_html) if badges_html else None))
cells_html = "".join(cells)
arrows_html = "".join(nav_arrows)
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))
@@ -632,7 +644,7 @@ def _entry_state_badge_html(state: str) -> str:
}
cls = state_classes.get(state, "bg-stone-100 text-stone-700")
label = state.replace("_", " ").capitalize()
return sx_call("events-state-badge", cls=cls, label=label)
return sx_call("badge", cls=cls, label=label)
# ---------------------------------------------------------------------------
@@ -693,12 +705,15 @@ def _markets_main_panel_html(ctx: dict) -> str:
form_html = ""
if can_create:
create_url = url_for("markets.create_market")
form_html = sx_call("events-markets-create-form",
create_url=create_url, csrf=csrf)
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("events-markets-panel",
form=SxExpr(form_html), list=SxExpr(list_html))
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:
@@ -710,7 +725,8 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
slug = post.get("slug", "")
if not markets:
return sx_call("events-markets-empty")
return sx_call("empty-state", message="No markets yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
for m in markets:
@@ -719,10 +735,13 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
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("events-markets-item",
href=market_href, market_name=m_name,
market_slug=m_slug, del_url=del_url,
csrf_hdr=csrf_hdr))
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)
@@ -740,7 +759,7 @@ def _ticket_state_badge_html(state: str) -> str:
}
cls = cls_map.get(state, "bg-stone-100 text-stone-700")
label = (state or "").replace("_", " ").capitalize()
return sx_call("events-state-badge", cls=cls, label=label)
return sx_call("badge", cls=cls, label=label)
# ---------------------------------------------------------------------------
@@ -1085,8 +1104,8 @@ def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
))
if has_more:
parts.append(sx_call("events-sentinel",
page=str(page), next_url=next_url))
parts.append(sx_call("sentinel-simple",
id=f"sentinel-{page}", next_url=next_url))
return "".join(parts)
@@ -1101,14 +1120,14 @@ _TILE_SVG = None
def _get_list_svg():
global _LIST_SVG
if _LIST_SVG is None:
_LIST_SVG = sx_call("events-list-svg")
_LIST_SVG = sx_call("list-svg")
return _LIST_SVG
def _get_tile_svg():
global _TILE_SVG
if _TILE_SVG is None:
_TILE_SVG = sx_call("events-tile-svg")
_TILE_SVG = sx_call("tile-svg")
return _TILE_SVG
@@ -1131,11 +1150,11 @@ def _view_toggle_html(ctx: dict, view: str) -> str:
list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600'
tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600'
return sx_call("events-view-toggle",
return sx_call("view-toggle",
list_href=list_href, tile_href=tile_href,
hx_select=hx_select, list_active=list_active,
tile_active=tile_active, list_svg=_get_list_svg(),
tile_svg=_get_tile_svg())
hx_select=hx_select, list_cls=list_active,
tile_cls=tile_active, storage_key="events_view",
list_svg=_get_list_svg(), tile_svg=_get_tile_svg())
def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
@@ -1154,7 +1173,9 @@ def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_
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("events-empty")
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=SxExpr(toggle), body=SxExpr(body))
@@ -1462,7 +1483,7 @@ async def render_slots_page(ctx: dict) -> str:
slots = ctx.get("slots") or []
calendar = ctx.get("calendar")
content = render_slots_table(slots, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
@@ -1477,7 +1498,7 @@ async def render_slots_oob(ctx: dict) -> str:
slots = ctx.get("slots") or []
calendar = ctx.get("calendar")
content = render_slots_table(slots, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_admin_header_sx(ctx, oob=True))
@@ -3012,7 +3033,7 @@ async def render_slot_page(ctx: dict) -> str:
if not slot or not calendar:
return ""
content = render_slot_main_panel(slot, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
@@ -3030,7 +3051,7 @@ async def render_slot_oob(ctx: dict) -> str:
if not slot or not calendar:
return ""
content = render_slot_main_panel(slot, calendar)
ctx = await _ensure_container_nav(ctx)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_admin_header_sx(ctx, oob=True))
@@ -3465,15 +3486,16 @@ def render_fragment_account_tickets(tickets) -> str:
type_name = ""
if getattr(ticket, "ticket_type_name", None):
type_name = f'<span>&middot; {escape(ticket.ticket_type_name)}</span>'
badge_html = sx_call("events-frag-ticket-badge",
state=getattr(ticket, "state", ""))
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("events-frag-tickets-empty")
body = sx_call("empty-state", message="No tickets yet.",
cls="text-sm text-stone-500")
return sx_call("events-frag-tickets-panel", items=SxExpr(body))
@@ -3498,8 +3520,8 @@ def render_fragment_account_bookings(bookings) -> str:
cost_str = ""
if getattr(booking, "cost", None):
cost_str = f'<span>&middot; &pound;{escape(str(booking.cost))}</span>'
badge_html = sx_call("events-frag-booking-badge",
state=getattr(booking, "state", ""))
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,
@@ -3507,6 +3529,7 @@ def render_fragment_account_bookings(bookings) -> str:
badge=badge_html)
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
else:
body = sx_call("events-frag-bookings-empty")
body = sx_call("empty-state", message="No bookings yet.",
cls="text-sm text-stone-500")
return sx_call("events-frag-bookings-panel", items=SxExpr(body))