diff --git a/events/bp/all_events/routes.py b/events/bp/all_events/routes.py
index c99cee5..6ca913c 100644
--- a/events/bp/all_events/routes.py
+++ b/events/bp/all_events/routes.py
@@ -67,7 +67,7 @@ def register() -> Blueprint:
entries, has_more, pending_tickets, page_info = await _load_entries(page)
from shared.sx.page import get_template_context
- from sxc.pages import render_all_events_page, render_all_events_oob
+ from sxc.pages.renders import render_all_events_page, render_all_events_oob
ctx = await get_template_context()
if is_htmx_request():
@@ -84,7 +84,7 @@ def register() -> Blueprint:
entries, has_more, pending_tickets, page_info = await _load_entries(page)
- from sxc.pages import render_all_events_cards
+ from sxc.pages.renders import render_all_events_cards
sx_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view)
return sx_response(sx_src)
@@ -125,7 +125,7 @@ def register() -> Blueprint:
if ident["session_id"] is not None:
frag_params["session_id"] = ident["session_id"]
- from sxc.pages import render_ticket_widget
+ from sxc.pages.renders import render_ticket_widget
widget_html = await render_ticket_widget(entry, qty, "/all-tickets/adjust")
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sx_response(widget_html + (mini_html or ""))
diff --git a/events/bp/calendar/admin/routes.py b/events/bp/calendar/admin/routes.py
index 606996d..123e3ff 100644
--- a/events/bp/calendar/admin/routes.py
+++ b/events/bp/calendar/admin/routes.py
@@ -18,7 +18,7 @@ def register():
@bp.get("/description/")
@require_admin
async def calendar_description_edit(calendar_slug: str, **kwargs):
- from sxc.pages import render_calendar_description_edit
+ from sxc.pages.renders import render_calendar_description_edit
html = await render_calendar_description_edit(g.calendar)
return sx_response(html)
@@ -34,7 +34,7 @@ def register():
g.calendar.description = description
await g.s.flush()
- from sxc.pages import render_calendar_description
+ from sxc.pages.renders import render_calendar_description
html = await render_calendar_description(g.calendar, oob=True)
return sx_response(html)
@@ -42,7 +42,7 @@ def register():
@bp.get("/description/view/")
@require_admin
async def calendar_description_view(calendar_slug: str, **kwargs):
- from sxc.pages import render_calendar_description
+ from sxc.pages.renders import render_calendar_description
html = await render_calendar_description(g.calendar)
return sx_response(html)
diff --git a/events/bp/calendar/routes.py b/events/bp/calendar/routes.py
index b7525a1..51e2900 100644
--- a/events/bp/calendar/routes.py
+++ b/events/bp/calendar/routes.py
@@ -158,7 +158,7 @@ def register():
confirmed_entries = visible.confirmed_entries
from shared.sx.page import get_template_context
- from sxc.pages import render_calendar_page, render_calendar_oob
+ from sxc.pages.renders import render_calendar_page, render_calendar_oob
tctx = await get_template_context()
tctx.update(dict(
@@ -199,7 +199,7 @@ def register():
await update_calendar_description(g.calendar, description)
from shared.sx.page import get_template_context
- from sxc.pages import _calendar_admin_main_panel_html
+ from sxc.pages.calendar import _calendar_admin_main_panel_html
ctx = await get_template_context()
html = await _calendar_admin_main_panel_html(ctx)
return sx_response(html)
@@ -218,13 +218,13 @@ def register():
# If we have post context (blog-embedded mode), update nav
post_data = getattr(g, "post_data", None)
from shared.sx.page import get_template_context
- from sxc.pages import render_calendars_list_panel
+ from sxc.pages.renders import render_calendars_list_panel
ctx = await get_template_context()
html = await render_calendars_list_panel(ctx)
if post_data:
from shared.services.entry_associations import get_associated_entries
- from sxc.pages import render_post_nav_entries_oob
+ from sxc.pages.renders import render_post_nav_entries_oob
post_id = (post_data.get("post") or {}).get("id")
cals = (
diff --git a/events/bp/calendar_entries/routes.py b/events/bp/calendar_entries/routes.py
index 9c5a6a3..ddc3008 100644
--- a/events/bp/calendar_entries/routes.py
+++ b/events/bp/calendar_entries/routes.py
@@ -258,7 +258,7 @@ def register():
"styles": styles,
}
- from sxc.pages import render_day_main_panel
+ from sxc.pages.renders import render_day_main_panel
html = await render_day_main_panel(ctx)
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sx_response(html + (mini_html or ""))
@@ -279,12 +279,12 @@ def register():
result = await g.s.execute(stmt)
day_slots = list(result.scalars())
- from sxc.pages import render_entry_add_form
+ from sxc.pages.renders import render_entry_add_form
return sx_response(await render_entry_add_form(g.calendar, day, month, year, day_slots))
@bp.get("/add-button/")
async def add_button(day: int, month: int, year: int, **kwargs):
- from sxc.pages import render_entry_add_button
+ from sxc.pages.renders import render_entry_add_button
return sx_response(await render_entry_add_button(g.calendar, day, month, year))
diff --git a/events/bp/calendar_entry/routes.py b/events/bp/calendar_entry/routes.py
index f39c8be..fdbc53a 100644
--- a/events/bp/calendar_entry/routes.py
+++ b/events/bp/calendar_entry/routes.py
@@ -111,7 +111,7 @@ def register():
)
# Render OOB nav
- from sxc.pages import render_day_entries_nav_oob
+ from sxc.pages.renders import render_day_entries_nav_oob
return await render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date)
async def get_post_nav_oob(entry_id: int):
@@ -148,7 +148,7 @@ def register():
).scalars().all()
# Render OOB nav for this post
- from sxc.pages import render_post_nav_entries_oob
+ from sxc.pages.renders import render_post_nav_entries_oob
nav_oob = await render_post_nav_entries_oob(associated_entries, calendars, post)
nav_oobs.append(nav_oob)
@@ -256,7 +256,7 @@ def register():
result = await g.s.execute(stmt)
day_slots = list(result.scalars())
- from sxc.pages import render_entry_edit_form
+ from sxc.pages.renders import render_entry_edit_form
return sx_response(await render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots))
@bp.put("/")
@@ -420,7 +420,7 @@ def register():
nav_oob = await get_day_nav_oob(year, month, day)
from shared.sx.page import get_template_context
- from sxc.pages import _entry_main_panel_html
+ from sxc.pages.entries import _entry_main_panel_html
tctx = await get_template_context()
html = await _entry_main_panel_html(tctx)
@@ -448,7 +448,7 @@ def register():
# Re-read entry to get updated state
await g.s.refresh(g.entry)
- from sxc.pages import render_entry_optioned
+ from sxc.pages.renders import render_entry_optioned
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
return sx_response(html + day_nav_oob + post_nav_oob)
@@ -473,7 +473,7 @@ def register():
# Re-read entry to get updated state
await g.s.refresh(g.entry)
- from sxc.pages import render_entry_optioned
+ from sxc.pages.renders import render_entry_optioned
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
return sx_response(html + day_nav_oob + post_nav_oob)
@@ -498,7 +498,7 @@ def register():
# Re-read entry to get updated state
await g.s.refresh(g.entry)
- from sxc.pages import render_entry_optioned
+ from sxc.pages.renders import render_entry_optioned
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
return sx_response(html + day_nav_oob + post_nav_oob)
@@ -542,7 +542,7 @@ def register():
# Return just the tickets fragment (targeted by hx-target="#entry-tickets-...")
await g.s.refresh(g.entry)
- from sxc.pages import render_entry_tickets_config
+ from sxc.pages.renders import render_entry_tickets_config
html = await render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
return sx_response(html)
@@ -558,7 +558,7 @@ def register():
total_pages = math.ceil(total / per_page) if total > 0 else 0
va = request.view_args or {}
- from sxc.pages import render_post_search_results
+ from sxc.pages.renders import render_post_search_results
return sx_response(await render_post_search_results(
search_posts, query, page, total_pages,
g.entry, g.calendar,
@@ -592,7 +592,7 @@ def register():
entry_posts = await get_entry_posts(g.s, entry_id)
# Return updated posts list + OOB nav update
- from sxc.pages import render_entry_posts_panel, render_entry_posts_nav_oob
+ from sxc.pages.renders import render_entry_posts_panel, render_entry_posts_nav_oob
va = request.view_args or {}
html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = await render_entry_posts_nav_oob(entry_posts)
@@ -614,7 +614,7 @@ def register():
entry_posts = await get_entry_posts(g.s, entry_id)
# Return updated posts list + OOB nav update
- from sxc.pages import render_entry_posts_panel, render_entry_posts_nav_oob
+ from sxc.pages.renders import render_entry_posts_panel, render_entry_posts_nav_oob
va = request.view_args or {}
html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = await render_entry_posts_nav_oob(entry_posts)
diff --git a/events/bp/calendars/routes.py b/events/bp/calendars/routes.py
index 336425c..b3cd72e 100644
--- a/events/bp/calendars/routes.py
+++ b/events/bp/calendars/routes.py
@@ -32,7 +32,7 @@ def register():
@cache_page(tag="calendars")
async def home(**kwargs):
from shared.sx.page import get_template_context
- from sxc.pages import render_calendars_page, render_calendars_oob
+ from sxc.pages.renders import render_calendars_page, render_calendars_oob
ctx = await get_template_context()
if not is_htmx_request():
@@ -67,14 +67,14 @@ def register():
return await make_response(render_comp("error-inline", message=str(e)), 422)
from shared.sx.page import get_template_context
- from sxc.pages import render_calendars_list_panel
+ from sxc.pages.renders import render_calendars_list_panel
ctx = await get_template_context()
html = await render_calendars_list_panel(ctx)
# Blog-embedded mode: also update post nav
if post_data:
from shared.services.entry_associations import get_associated_entries
- from sxc.pages import render_post_nav_entries_oob
+ from sxc.pages.renders import render_post_nav_entries_oob
cals = (
await g.s.execute(
diff --git a/events/bp/day/routes.py b/events/bp/day/routes.py
index e1588c8..8c431af 100644
--- a/events/bp/day/routes.py
+++ b/events/bp/day/routes.py
@@ -123,7 +123,7 @@ def register():
- pending only for current user/session
"""
from shared.sx.page import get_template_context
- from sxc.pages import render_day_page, render_day_oob
+ from sxc.pages.renders import render_day_page, render_day_oob
tctx = await get_template_context()
if not is_htmx_request():
diff --git a/events/bp/markets/routes.py b/events/bp/markets/routes.py
index ac6a593..415f6c0 100644
--- a/events/bp/markets/routes.py
+++ b/events/bp/markets/routes.py
@@ -42,7 +42,7 @@ def register():
return await make_response(render_comp("error-inline", message=str(e)), 422)
from shared.sx.page import get_template_context
- from sxc.pages import render_markets_list_panel
+ from sxc.pages.renders import render_markets_list_panel
ctx = await get_template_context()
return sx_response(await render_markets_list_panel(ctx))
@@ -55,7 +55,7 @@ def register():
return await make_response("Market not found", 404)
from shared.sx.page import get_template_context
- from sxc.pages import render_markets_list_panel
+ from sxc.pages.renders import render_markets_list_panel
ctx = await get_template_context()
return sx_response(await render_markets_list_panel(ctx))
diff --git a/events/bp/page/routes.py b/events/bp/page/routes.py
index 7366fa1..2f4ac06 100644
--- a/events/bp/page/routes.py
+++ b/events/bp/page/routes.py
@@ -47,7 +47,7 @@ def register() -> Blueprint:
entries, has_more, pending_tickets = await _load_entries(post["id"], page)
from shared.sx.page import get_template_context
- from sxc.pages import render_page_summary_page, render_page_summary_oob
+ from sxc.pages.renders import render_page_summary_page, render_page_summary_oob
ctx = await get_template_context()
if is_htmx_request():
@@ -65,7 +65,7 @@ def register() -> Blueprint:
entries, has_more, pending_tickets = await _load_entries(post["id"], page)
- from sxc.pages import render_page_summary_cards
+ from sxc.pages.renders import render_page_summary_cards
sx_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post)
return sx_response(sx_src)
@@ -106,7 +106,7 @@ def register() -> Blueprint:
if ident["session_id"] is not None:
frag_params["session_id"] = ident["session_id"]
- from sxc.pages import render_ticket_widget
+ from sxc.pages.renders import render_ticket_widget
widget_html = await render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust")
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sx_response(widget_html + (mini_html or ""))
diff --git a/events/bp/slot/routes.py b/events/bp/slot/routes.py
index 7b821c0..dea7507 100644
--- a/events/bp/slot/routes.py
+++ b/events/bp/slot/routes.py
@@ -35,7 +35,7 @@ def register():
slot = await svc_get_slot(g.s, slot_id)
if not slot:
return await make_response("Not found", 404)
- from sxc.pages import render_slot_edit_form
+ from sxc.pages.renders import render_slot_edit_form
return sx_response(await render_slot_edit_form(slot, g.calendar))
@bp.get("/view/")
@@ -44,7 +44,7 @@ def register():
slot = await svc_get_slot(g.s, slot_id)
if not slot:
return await make_response("Not found", 404)
- from sxc.pages import render_slot_main_panel
+ from sxc.pages.renders import render_slot_main_panel
return sx_response(await render_slot_main_panel(slot, g.calendar))
@bp.delete("/")
@@ -53,7 +53,7 @@ def register():
async def slot_delete(slot_id: int, **kwargs):
await svc_delete_slot(g.s, slot_id)
slots = await svc_list_slots(g.s, g.calendar.id)
- from sxc.pages import render_slots_table
+ from sxc.pages.renders import render_slots_table
return sx_response(await render_slots_table(slots, g.calendar))
@bp.put("/")
@@ -135,7 +135,7 @@ def register():
}
), 422
- from sxc.pages import render_slot_main_panel
+ from sxc.pages.renders import render_slot_main_panel
return sx_response(await render_slot_main_panel(slot, g.calendar, oob=True))
diff --git a/events/bp/slots/routes.py b/events/bp/slots/routes.py
index 708db6a..41cb73b 100644
--- a/events/bp/slots/routes.py
+++ b/events/bp/slots/routes.py
@@ -110,20 +110,20 @@ def register():
# Success → re-render the slots table
slots = await svc_list_slots(g.s, g.calendar.id)
- from sxc.pages import render_slots_table
+ from sxc.pages.renders import render_slots_table
return sx_response(await render_slots_table(slots, g.calendar))
@bp.get("/add")
@require_admin
async def add_form(**kwargs):
- from sxc.pages import render_slot_add_form
+ from sxc.pages.renders import render_slot_add_form
return sx_response(await render_slot_add_form(g.calendar))
@bp.get("/add-button")
@require_admin
async def add_button(**kwargs):
- from sxc.pages import render_slot_add_button
+ from sxc.pages.renders import render_slot_add_button
return sx_response(await render_slot_add_button(g.calendar))
return bp
diff --git a/events/bp/ticket_admin/routes.py b/events/bp/ticket_admin/routes.py
index 3c8b4b5..d519b70 100644
--- a/events/bp/ticket_admin/routes.py
+++ b/events/bp/ticket_admin/routes.py
@@ -53,7 +53,7 @@ def register() -> Blueprint:
tickets = await get_tickets_for_entry(g.s, entry_id)
- from sxc.pages import render_entry_tickets_admin
+ from sxc.pages.renders import render_entry_tickets_admin
html = await render_entry_tickets_admin(entry, tickets)
return sx_response(html)
@@ -69,7 +69,7 @@ def register() -> Blueprint:
)
ticket = await get_ticket_by_code(g.s, code)
- from sxc.pages import render_lookup_result
+ from sxc.pages.renders import render_lookup_result
if not ticket:
return sx_response(await render_lookup_result(None, "Ticket not found"))
@@ -82,7 +82,7 @@ def register() -> Blueprint:
"""Check in a ticket by its code."""
success, error = await checkin_ticket(g.s, code)
- from sxc.pages import render_checkin_result
+ from sxc.pages.renders import render_checkin_result
if not success:
return sx_response(await render_checkin_result(False, error, None))
diff --git a/events/bp/ticket_type/routes.py b/events/bp/ticket_type/routes.py
index 608c198..64dbfd6 100644
--- a/events/bp/ticket_type/routes.py
+++ b/events/bp/ticket_type/routes.py
@@ -30,7 +30,7 @@ def register():
if not ticket_type:
return await make_response("Not found", 404)
- from sxc.pages import render_ticket_type_edit_form
+ from sxc.pages.renders import render_ticket_type_edit_form
va = request.view_args or {}
return sx_response(await render_ticket_type_edit_form(
ticket_type, g.entry, g.calendar,
@@ -45,7 +45,7 @@ def register():
if not ticket_type:
return await make_response("Not found", 404)
- from sxc.pages import render_ticket_type_main_panel
+ from sxc.pages.renders import render_ticket_type_main_panel
va = request.view_args or {}
return sx_response(await render_ticket_type_main_panel(
ticket_type, g.entry, g.calendar,
@@ -112,7 +112,7 @@ def register():
return await make_response("Not found", 404)
# Return updated view with OOB flag
- from sxc.pages import render_ticket_type_main_panel
+ from sxc.pages.renders import render_ticket_type_main_panel
va = request.view_args or {}
return sx_response(await render_ticket_type_main_panel(
ticket_type, g.entry, g.calendar,
@@ -131,7 +131,7 @@ def register():
# Re-render the ticket types list
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
- from sxc.pages import render_ticket_types_table
+ from sxc.pages.renders import render_ticket_types_table
va = request.view_args or {}
return sx_response(await render_ticket_types_table(
ticket_types, g.entry, g.calendar,
diff --git a/events/bp/ticket_types/routes.py b/events/bp/ticket_types/routes.py
index a11eedc..e85e82d 100644
--- a/events/bp/ticket_types/routes.py
+++ b/events/bp/ticket_types/routes.py
@@ -93,7 +93,7 @@ def register():
# Success → re-render the ticket types table
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
- from sxc.pages import render_ticket_types_table
+ from sxc.pages.renders import render_ticket_types_table
va = request.view_args or {}
return sx_response(await render_ticket_types_table(
ticket_types, g.entry, g.calendar,
@@ -104,7 +104,7 @@ def register():
@require_admin
async def add_form(**kwargs):
"""Show the add ticket type form."""
- from sxc.pages import render_ticket_type_add_form
+ from sxc.pages.renders import render_ticket_type_add_form
va = request.view_args or {}
return sx_response(await render_ticket_type_add_form(
g.entry, g.calendar,
@@ -115,7 +115,7 @@ def register():
@require_admin
async def add_button(**kwargs):
"""Show the add ticket type button."""
- from sxc.pages import render_ticket_type_add_button
+ from sxc.pages.renders import render_ticket_type_add_button
va = request.view_args or {}
return sx_response(await render_ticket_type_add_button(
g.entry, g.calendar,
diff --git a/events/bp/tickets/routes.py b/events/bp/tickets/routes.py
index 6d43ba4..d884ca5 100644
--- a/events/bp/tickets/routes.py
+++ b/events/bp/tickets/routes.py
@@ -126,7 +126,7 @@ def register() -> Blueprint:
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
cart_count = summary.count + summary.calendar_count + summary.ticket_count
- from sxc.pages import render_buy_result
+ from sxc.pages.renders import render_buy_result
return sx_response(await render_buy_result(entry, created, remaining, cart_count))
@bp.post("/adjust/")
@@ -249,7 +249,7 @@ def register() -> Blueprint:
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
cart_count = summary.count + summary.calendar_count + summary.ticket_count
- from sxc.pages import render_adjust_response
+ from sxc.pages.renders import render_adjust_response
return sx_response(await render_adjust_response(
entry, ticket_remaining, ticket_sold_count,
user_ticket_count, user_ticket_counts_by_type, cart_count,
diff --git a/events/sxc/pages/__init__.py b/events/sxc/pages/__init__.py
index 5c38499..7052bc5 100644
--- a/events/sxc/pages/__init__.py
+++ b/events/sxc/pages/__init__.py
@@ -1,22 +1,11 @@
"""Events defpage setup — registers layouts, page helpers, and loads .sx pages."""
from __future__ import annotations
-from typing import Any
-from markupsafe import escape
-
-from shared.sx.parser import SxExpr
-from shared.sx.helpers import (
- call_url, get_asset_url, render_to_sx,
- render_to_sx_with_env, _ctx_to_env,
- post_header_sx, post_admin_header_sx,
- oob_header_sx, header_child_sx,
- full_page_sx, oob_page_sx,
- search_mobile_sx, search_desktop_sx,
-)
-
def setup_events_pages() -> None:
"""Register events-specific layouts, page helpers, and load page definitions."""
+ from .layouts import _register_events_layouts
+ from .helpers import _register_events_helpers
_register_events_layouts()
_register_events_helpers()
_load_events_page_files()
@@ -26,3836 +15,7 @@ def _load_events_page_files() -> None:
import os
from shared.sx.pages import load_page_dir
from shared.sx.jinja_bridge import load_service_components
- sxc_dir = os.path.dirname(os.path.dirname(__file__)) # events/sxc/
- service_root = os.path.dirname(sxc_dir) # events/
+ sxc_dir = os.path.dirname(os.path.dirname(__file__))
+ service_root = os.path.dirname(sxc_dir)
load_service_components(service_root, service_name="events")
load_page_dir(os.path.dirname(__file__), "events")
-
-
-# ---------------------------------------------------------------------------
-# OOB header helper — delegates to shared
-# ---------------------------------------------------------------------------
-
-
-
-# ---------------------------------------------------------------------------
-# 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."""
- 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)
-_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"):
- return ctx
- post = ctx.get("post") or {}
- post_id = post.get("id")
- 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),
- ("market", "container-nav", nav_params),
- ], required=False)
- return {**ctx, "container_nav": events_nav + market_nav}
-
-
-async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build the post-level header row — delegates to shared sx helper."""
- return await post_header_sx(ctx, oob=oob)
-
-
-async 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 []
- select_colours = ctx.get("select_colours", "")
- current_cal_slug = getattr(g, "calendar_slug", None)
-
- parts = []
- for cal in calendars:
- 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(await render_to_sx("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)
-
- # 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)
- 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'
'
- )
-
- return "".join(parts)
-
-
-# ---------------------------------------------------------------------------
-# Post admin header
-# ---------------------------------------------------------------------------
-
-# ---------------------------------------------------------------------------
-# Calendars header
-# ---------------------------------------------------------------------------
-
-async def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build the calendars section header row."""
- from quart import url_for
- link_href = url_for("calendars.home")
- return await render_to_sx("menu-row-sx", id="calendars-row", level=3,
- link_href=link_href,
- link_label_content=SxExpr(await render_to_sx("events-calendars-label")),
- child_id="calendars-header-child", oob=oob)
-
-
-# ---------------------------------------------------------------------------
-# Calendar header
-# ---------------------------------------------------------------------------
-
-async def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build a single calendar's header row."""
- from quart import url_for
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- cal_name = getattr(calendar, "name", "")
- cal_desc = getattr(calendar, "description", "") or ""
-
- link_href = url_for("calendar.get", calendar_slug=cal_slug)
- label_html = await render_to_sx("events-calendar-label",
- name=cal_name, description=cal_desc)
-
- # Desktop nav: slots + admin
- nav_html = await _calendar_nav_sx(ctx)
-
- return await render_to_sx("menu-row-sx", id="calendar-row", level=3,
- link_href=link_href, link_label_content=SxExpr(label_html),
- nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob)
-
-
-async def _calendar_nav_sx(ctx: dict) -> str:
- """Calendar desktop nav: Slots + admin link."""
- from quart import url_for
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- rights = ctx.get("rights") or {}
- 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(await render_to_sx("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(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog",
- select_colours=select_colours))
- return "(<> " + " ".join(parts) + ")" if parts else ""
-
-
-# ---------------------------------------------------------------------------
-# Day header
-# ---------------------------------------------------------------------------
-
-async def _day_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build day detail header row."""
- from quart import url_for
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- day_date = ctx.get("day_date")
- if not day_date:
- return ""
-
- link_href = url_for(
- "calendar.day.show_day",
- calendar_slug=cal_slug,
- year=day_date.year,
- month=day_date.month,
- day=day_date.day,
- )
- label_html = await render_to_sx("events-day-label",
- date_str=day_date.strftime("%A %d %B %Y"))
-
- nav_html = await _day_nav_sx(ctx)
-
- return await render_to_sx("menu-row-sx", id="day-row", level=4,
- link_href=link_href, link_label_content=SxExpr(label_html),
- nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob)
-
-
-async def _day_nav_sx(ctx: dict) -> str:
- """Day desktop nav: confirmed entries scrolling menu + admin link."""
- from quart import url_for
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- day_date = ctx.get("day_date")
- confirmed_entries = ctx.get("confirmed_entries") or []
- 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(
- "calendar.day.calendar_entries.calendar_entry.get",
- 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(await render_to_sx("events-day-entry-link",
- href=href, name=entry.name,
- time_str=f"{start}{end}"))
- inner = "".join(entry_links)
- parts.append(await render_to_sx("events-day-entries-nav", inner=SxExpr(inner)))
-
- if is_admin and day_date:
- admin_href = url_for(
- "calendar.day.admin.admin",
- calendar_slug=cal_slug,
- year=day_date.year,
- month=day_date.month,
- day=day_date.day,
- )
- parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog"))
- return "".join(parts)
-
-
-# ---------------------------------------------------------------------------
-# Day admin header
-# ---------------------------------------------------------------------------
-
-async def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build day admin header row."""
- from quart import url_for
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- day_date = ctx.get("day_date")
- if not day_date:
- return ""
-
- link_href = url_for(
- "calendar.day.admin.admin",
- calendar_slug=cal_slug,
- year=day_date.year,
- month=day_date.month,
- day=day_date.day,
- )
- return await render_to_sx("menu-row-sx", id="day-admin-row", level=5,
- link_href=link_href, link_label="admin", icon="fa fa-cog",
- child_id="day-admin-header-child", oob=oob)
-
-
-# ---------------------------------------------------------------------------
-# Calendar admin header
-# ---------------------------------------------------------------------------
-
-async def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build calendar admin header row with nav links."""
- from quart import url_for
- calendar = ctx.get("calendar")
- cal_slug = getattr(calendar, "slug", "") if calendar else ""
- select_colours = ctx.get("select_colours", "")
-
- nav_parts = []
- 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(await render_to_sx("nav-link", href=href, label=label,
- select_colours=select_colours))
-
- nav_html = "".join(nav_parts)
- return await render_to_sx("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)
-
-
-# ---------------------------------------------------------------------------
-# Markets header
-# ---------------------------------------------------------------------------
-
-async def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
- """Build the markets section header row."""
- from quart import url_for
- link_href = url_for("defpage_events_markets")
- return await render_to_sx("menu-row-sx", id="markets-row", level=3,
- link_href=link_href,
- link_label_content=SxExpr(await render_to_sx("events-markets-label")),
- child_id="markets-header-child", oob=oob)
-
-
-# ---------------------------------------------------------------------------
-# Calendars main panel
-# ---------------------------------------------------------------------------
-
-async def _calendars_main_panel_sx(ctx: dict) -> str:
- """Render the calendars list + create form panel."""
- 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)
- 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 = await render_to_sx("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 = await _calendars_list_sx(ctx, calendars)
- return await render_to_sx("crud-panel",
- form=SxExpr(form_html), list=SxExpr(list_html),
- list_id="calendars-list")
-
-
-async 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 await render_to_sx("empty-state", message="No calendars yet. Create one above.",
- cls="text-gray-500 mt-4")
-
- parts = []
- 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(await render_to_sx("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)
-
-
-# ---------------------------------------------------------------------------
-# Calendar month grid
-# ---------------------------------------------------------------------------
-
-async def _calendar_main_panel_html(ctx: dict) -> str:
- """Render the calendar month grid."""
- from quart import url_for
- from quart import session as qsession
-
- calendar = ctx.get("calendar")
- 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", "")
-
- year = ctx.get("year", 2024)
- month = ctx.get("month", 1)
- month_name = ctx.get("month_name", "")
- weekday_names = ctx.get("weekday_names", [])
- weeks = ctx.get("weeks", [])
- prev_month = ctx.get("prev_month", 1)
- prev_month_year = ctx.get("prev_month_year", year)
- next_month = ctx.get("next_month", 1)
- next_month_year = ctx.get("next_month_year", year)
- prev_year = ctx.get("prev_year", year - 1)
- next_year = ctx.get("next_year", year + 1)
- month_entries = ctx.get("month_entries") or []
- user = ctx.get("user")
- qs = qsession if "qsession" not in ctx else ctx["qsession"]
-
- 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(await render_to_sx("events-calendar-nav-arrow",
- pill_cls=pill_cls, href=href, label=label))
-
- nav_arrows.append(await render_to_sx("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(await render_to_sx("events-calendar-nav-arrow",
- pill_cls=pill_cls, href=href, label=label))
-
- # Weekday headers
- wd_parts = []
- for wd in weekday_names:
- wd_parts.append(await render_to_sx("events-calendar-weekday", name=wd))
- wd_html = "".join(wd_parts)
-
- # Day cells
- cells = []
- for week in weeks:
- for day_cell in week:
- if isinstance(day_cell, dict):
- in_month = day_cell.get("in_month", True)
- is_today = day_cell.get("is_today", False)
- day_date = day_cell.get("date")
- else:
- in_month = getattr(day_cell, "in_month", True)
- is_today = getattr(day_cell, "is_today", False)
- day_date = getattr(day_cell, "date", None)
-
- cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs"
- if not in_month:
- cell_cls += " bg-stone-50 text-stone-400"
- if is_today:
- cell_cls += " ring-2 ring-blue-500 z-10 relative"
-
- # Day number link
- day_num_html = ""
- day_short_html = ""
- if day_date:
- 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 = await render_to_sx("events-calendar-day-short",
- day_str=day_date.strftime("%a"))
- day_num_html = await render_to_sx("events-calendar-day-num",
- pill_cls=pill_cls, href=day_href,
- num=str(day_date.day))
-
- # Entry badges for this day
- entry_badges = []
- if day_date:
- for e in month_entries:
- if e.start_at and e.start_at.date() == day_date:
- is_mine = (
- (user and e.user_id == user.id)
- or (not user and e.session_id == qs.get("calendar_sid"))
- )
- if e.state == "confirmed":
- 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(await render_to_sx("events-calendar-entry-badge",
- bg_cls=bg_cls, name=e.name,
- state_label=state_label))
-
- badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else ""
- cells.append(await render_to_sx("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_html = "(<> " + "".join(cells) + ")"
- arrows_html = "(<> " + "".join(nav_arrows) + ")"
- wd_html = "(<> " + wd_html + ")"
- return await render_to_sx("events-calendar-grid",
- arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html),
- cells=SxExpr(cells_html))
-
-
-# ---------------------------------------------------------------------------
-# Day main panel
-# ---------------------------------------------------------------------------
-
-async def _day_main_panel_html(ctx: dict) -> str:
- """Render the day entries table + add button."""
- from quart import url_for
-
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- day_entries = ctx.get("day_entries") or []
- 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(await _day_row_html(ctx, entry))
- rows_html = "".join(row_parts)
- else:
- rows_html = await render_to_sx("events-day-empty-row")
-
- add_url = url_for(
- "calendar.day.calendar_entries.add_form",
- calendar_slug=cal_slug,
- day=day, month=month, year=year,
- )
-
- return await render_to_sx("events-day-table",
- list_container=list_container, rows=SxExpr(rows_html),
- pre_action=pre_action, add_url=add_url)
-
-
-async 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(
- "calendar.day.calendar_entries.calendar_entry.get",
- calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
- )
-
- # Name
- name_html = await render_to_sx("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 = await render_to_sx("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 = await render_to_sx("events-day-row-time", start=start, end=end)
-
- # State
- state = getattr(entry, "state", "pending") or "pending"
- state_badge = await _entry_state_badge_html(state)
- state_td = await render_to_sx("events-day-row-state",
- state_id=f"entry-state-{entry.id}", badge=SxExpr(state_badge))
-
- # Cost
- cost = getattr(entry, "cost", None)
- cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
- cost_td = await render_to_sx("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 = await render_to_sx("events-day-row-tickets",
- price_str=f"\u00a3{tp:.2f}", count_str=tc_str)
- else:
- tickets_td = await render_to_sx("events-day-row-no-tickets")
-
- actions_td = await render_to_sx("events-day-row-actions")
-
- return await render_to_sx("events-day-row",
- tr_cls=tr_cls, name=SxExpr(name_html), slot=SxExpr(slot_html),
- state=SxExpr(state_td), cost=SxExpr(cost_td),
- tickets=SxExpr(tickets_td), actions=SxExpr(actions_td))
-
-
-async def _entry_state_badge_html(state: str) -> str:
- """Render an entry state badge."""
- state_classes = {
- "confirmed": "bg-emerald-100 text-emerald-800",
- "provisional": "bg-amber-100 text-amber-800",
- "ordered": "bg-sky-100 text-sky-800",
- "pending": "bg-stone-100 text-stone-700",
- "declined": "bg-red-100 text-red-800",
- }
- cls = state_classes.get(state, "bg-stone-100 text-stone-700")
- label = state.replace("_", " ").capitalize()
- return await render_to_sx("badge", cls=cls, label=label)
-
-
-# ---------------------------------------------------------------------------
-# Day admin main panel
-# ---------------------------------------------------------------------------
-
-async def _day_admin_main_panel_html(ctx: dict) -> str:
- """Render day admin panel (placeholder nav)."""
- return await render_to_sx("events-day-admin-panel")
-
-
-# ---------------------------------------------------------------------------
-# Calendar admin main panel
-# ---------------------------------------------------------------------------
-
-async def _calendar_admin_main_panel_html(ctx: dict) -> str:
- """Render calendar admin config panel with description editor."""
- from quart import url_for
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- csrf_token = ctx.get("csrf_token")
- csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
- cal_slug = getattr(calendar, "slug", "")
- desc = getattr(calendar, "description", "") or ""
- hx_select = ctx.get("hx_select_search", "#main-panel")
-
- desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
- description_html = await _calendar_description_display_html(calendar, desc_edit_url)
-
- return await render_to_sx("events-calendar-admin-panel",
- description_content=SxExpr(description_html), csrf=csrf,
- description=desc)
-
-
-async def _calendar_description_display_html(calendar, edit_url: str) -> str:
- """Render calendar description display with edit button."""
- desc = getattr(calendar, "description", "") or ""
- return await render_to_sx("events-calendar-description-display",
- description=desc, edit_url=edit_url)
-
-
-# ---------------------------------------------------------------------------
-# Markets main panel
-# ---------------------------------------------------------------------------
-
-async def _markets_main_panel_html(ctx: dict) -> str:
- """Render markets list + create form panel."""
- 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)
- has_access = ctx.get("has_access")
- can_create = has_access("markets.create_market") if callable(has_access) else is_admin
- 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 = await render_to_sx("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 = await _markets_list_html(ctx, markets)
- return await render_to_sx("crud-panel",
- form=SxExpr(form_html), list=SxExpr(list_html),
- list_id="markets-list")
-
-
-async 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 await render_to_sx("empty-state", message="No markets yet. Create one above.",
- cls="text-gray-500 mt-4")
-
- parts = []
- 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(await render_to_sx("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)
-
-
-# ---------------------------------------------------------------------------
-# Ticket state badge helper
-# ---------------------------------------------------------------------------
-
-async 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 await render_to_sx("badge", cls=cls, label=label)
-
-
-# ---------------------------------------------------------------------------
-# Tickets main panel (my tickets)
-# ---------------------------------------------------------------------------
-
-async def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
- """Render my tickets list."""
- from quart import url_for
-
- ticket_cards = []
- 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_cards.append(await render_to_sx("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=SxExpr(await _ticket_state_badge_html(state)),
- code_prefix=ticket.code[:8]))
-
- cards_html = "".join(ticket_cards)
- return await render_to_sx("events-tickets-panel",
- list_container=_list_container(ctx),
- has_tickets=bool(tickets), cards=SxExpr(cards_html))
-
-
-# ---------------------------------------------------------------------------
-# Ticket detail panel
-# ---------------------------------------------------------------------------
-
-async def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
- """Render a single ticket detail with QR code."""
- from quart import url_for
-
- entry = getattr(ticket, "entry", None)
- tt = getattr(ticket, "ticket_type", None)
- state = getattr(ticket, "state", "")
- code = ticket.code
- cal = getattr(entry, "calendar", None) if entry else None
- 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 = (await _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'){"
- "var cv=document.createElement('canvas');"
- f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});"
- "}})()"
- )
-
- return await render_to_sx("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,
- 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,
- qr_script=qr_script)
-
-
-# ---------------------------------------------------------------------------
-# Ticket admin main panel
-# ---------------------------------------------------------------------------
-
-async def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str:
- """Render ticket admin dashboard."""
- 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 += await render_to_sx("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 = ""
- for ticket in tickets:
- entry = getattr(ticket, "entry", None)
- tt = getattr(ticket, "ticket_type", None)
- state = getattr(ticket, "state", "")
- code = ticket.code
-
- date_html = ""
- if entry and entry.start_at:
- date_html = await render_to_sx("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 = await render_to_sx("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 = await render_to_sx("events-ticket-admin-checked-in",
- time_str=t_str)
-
- rows_html += await render_to_sx("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=SxExpr(await _ticket_state_badge_html(state)),
- action=SxExpr(action_html))
-
- return await render_to_sx("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))
-
-
-# ---------------------------------------------------------------------------
-# All events / page summary entry cards
-# ---------------------------------------------------------------------------
-
-async def _entry_card_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 list card for one event entry."""
- 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")
-
- 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 (linked or plain)
- if entry_href:
- title_html = await render_to_sx("events-entry-title-linked",
- href=entry_href, name=entry.name)
- else:
- title_html = await render_to_sx("events-entry-title-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 += await render_to_sx("events-entry-page-badge",
- href=page_href, title=page_title)
- cal_name = getattr(entry, "calendar_name", "")
- if cal_name:
- badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name)
-
- # Time line
- time_parts = ""
- if day_href and not is_page_scoped:
- time_parts += await render_to_sx("events-entry-time-linked",
- href=day_href,
- date_str=entry.start_at.strftime("%a %-d %b"))
- elif not is_page_scoped:
- time_parts += await render_to_sx("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")}'
-
- cost = getattr(entry, "cost", None)
- cost_html = await render_to_sx("events-entry-cost",
- cost=f"£{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 = await render_to_sx("events-entry-widget-wrapper",
- widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={})))
-
- return await render_to_sx("events-entry-card",
- title=SxExpr(title_html), badges=SxExpr(badges_html),
- time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
- widget=SxExpr(widget_html))
-
-
-async 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."""
- 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")
-
- 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 = await render_to_sx("events-entry-title-tile-linked",
- href=entry_href, name=entry.name)
- else:
- title_html = await render_to_sx("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 += await render_to_sx("events-entry-page-badge",
- href=page_href, title=page_title)
- cal_name = getattr(entry, "calendar_name", "")
- if cal_name:
- badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name)
-
- # Time
- time_html = ""
- if day_href:
- time_html += (await render_to_sx("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 = await render_to_sx("events-entry-cost",
- cost=f"£{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 = await render_to_sx("events-entry-tile-widget-wrapper",
- widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={})))
-
- return await render_to_sx("events-entry-card-tile",
- title=SxExpr(title_html), badges=SxExpr(badges_html),
- time=SxExpr(time_html), cost=SxExpr(cost_html),
- widget=SxExpr(widget_html))
-
-
-async 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
-
- eid = entry.id
- tp = getattr(entry, "ticket_price", 0) or 0
- tgt = f"#page-ticket-{eid}"
-
- async def _tw_form(count_val, btn_html):
- return await render_to_sx("events-tw-form",
- ticket_url=ticket_url, target=tgt,
- csrf=csrf_token_val, entry_id=str(eid),
- count_val=str(count_val), btn=SxExpr(btn_html))
-
- if qty == 0:
- inner = await _tw_form(1, await render_to_sx("events-tw-cart-plus"))
- else:
- minus = await _tw_form(qty - 1, await render_to_sx("events-tw-minus"))
- cart_icon = await render_to_sx("events-tw-cart-icon", qty=str(qty))
- plus = await _tw_form(qty + 1, await render_to_sx("events-tw-plus"))
- inner = minus + cart_icon + plus
-
- return await render_to_sx("events-tw-widget",
- entry_id=str(eid), price=f"£{tp:.2f}",
- inner=SxExpr(inner))
-
-
-async 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(await _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(await render_to_sx("events-date-separator",
- date_str=entry_date))
- last_date = entry_date
- parts.append(await _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(await render_to_sx("sentinel-simple",
- id=f"sentinel-{page}", next_url=next_url))
- return "".join(parts)
-
-
-# ---------------------------------------------------------------------------
-# All events / page summary main panels
-# ---------------------------------------------------------------------------
-
-_LIST_SVG = None
-_TILE_SVG = None
-
-
-async def _get_list_svg():
- global _LIST_SVG
- if _LIST_SVG is None:
- _LIST_SVG = await render_to_sx("list-svg")
- return _LIST_SVG
-
-
-async def _get_tile_svg():
- global _TILE_SVG
- if _TILE_SVG is None:
- _TILE_SVG = await render_to_sx("tile-svg")
- return _TILE_SVG
-
-
-async def _view_toggle_html(ctx: dict, view: str) -> str:
- """Render the list/tile view toggle bar."""
- from shared.utils import route_prefix
- prefix = route_prefix()
- clh = ctx.get("current_local_href", "/")
- hx_select = ctx.get("hx_select_search", "#main-panel")
-
- list_href = prefix + str(clh)
- tile_href = prefix + str(clh)
- if "?" in list_href:
- list_href = list_href.split("?")[0]
- if "?" in tile_href:
- tile_href = tile_href.split("?")[0] + "?view=tile"
- else:
- tile_href = tile_href + "?view=tile"
-
- 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 await render_to_sx("view-toggle",
- list_href=list_href, tile_href=tile_href,
- hx_select=hx_select, list_cls=list_active,
- tile_cls=tile_active, storage_key="events_view",
- list_svg=SxExpr(await _get_list_svg()), tile_svg=SxExpr(await _get_tile_svg()))
-
-
-async def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
- page, view, ticket_url, next_url, events_url_fn,
- *, is_page_scoped=False, post=None) -> str:
- """Render the events main panel with view toggle + cards."""
- toggle = await _view_toggle_html(ctx, view)
-
- if entries:
- cards = await _entry_cards_html(
- entries, page_info, pending_tickets, ticket_url, events_url_fn,
- view, page, has_more, next_url,
- 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 = await render_to_sx("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
- else:
- body = await render_to_sx("empty-state", icon="fa fa-calendar-xmark",
- message="No upcoming events",
- cls="px-3 py-12 text-center text-stone-400")
-
- return await render_to_sx("events-main-panel-body",
- toggle=SxExpr(toggle), body=SxExpr(body))
-
-
-# ---------------------------------------------------------------------------
-# Utility
-# ---------------------------------------------------------------------------
-
-def _list_container(ctx: dict) -> str:
- styles = ctx.get("styles") or {}
- return getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
-
-
-# ===========================================================================
-# PUBLIC API
-# ===========================================================================
-
-
-# ---------------------------------------------------------------------------
-# All events
-# ---------------------------------------------------------------------------
-
-async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets,
- page_info, page, view) -> str:
- """Full page: all events listing."""
- from quart import url_for
- from shared.utils import route_prefix
- from shared.infrastructure.urls import events_url
-
- prefix = route_prefix()
- view_param = f"&view={view}" if view != "list" else ""
- ticket_url = url_for("all_events.adjust_ticket")
- next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
-
- content = await _events_main_panel_html(
- ctx, entries, has_more, pending_tickets, page_info,
- page, view, ticket_url, next_url, events_url,
- )
- hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
- return await full_page_sx(ctx, header_rows=hdr, content=content)
-
-
-async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets,
- page_info, page, view) -> str:
- """OOB response: all events listing (htmx nav)."""
- from quart import url_for
- from shared.utils import route_prefix
- from shared.infrastructure.urls import events_url
-
- prefix = route_prefix()
- ticket_url = url_for("all_events.adjust_ticket")
- next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
-
- content = await _events_main_panel_html(
- ctx, entries, has_more, pending_tickets, page_info,
- page, view, ticket_url, next_url, events_url,
- )
- return await oob_page_sx(content=content)
-
-
-async def render_all_events_cards(entries, has_more, pending_tickets,
- page_info, page, view) -> str:
- """Pagination fragment: all events cards only."""
- from quart import url_for
- from shared.utils import route_prefix
- from shared.infrastructure.urls import events_url
-
- prefix = route_prefix()
- ticket_url = url_for("all_events.adjust_ticket")
- next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
-
- return await _entry_cards_html(
- entries, page_info, pending_tickets, ticket_url, events_url,
- view, page, has_more, next_url,
- )
-
-
-# ---------------------------------------------------------------------------
-# Page summary
-# ---------------------------------------------------------------------------
-
-async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets,
- page_info, page, view) -> str:
- """Full page: page-scoped events listing."""
- from quart import url_for
- from shared.utils import route_prefix
- from shared.infrastructure.urls import events_url
-
- prefix = route_prefix()
- post = ctx.get("post") or {}
- ticket_url = url_for("page_summary.adjust_ticket")
- next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
-
- content = await _events_main_panel_html(
- ctx, entries, has_more, pending_tickets, page_info,
- page, view, ticket_url, next_url, events_url,
- is_page_scoped=True, post=post,
- )
-
- hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
- hdr += await header_child_sx(await _post_header_sx(ctx))
- return await full_page_sx(ctx, header_rows=hdr, content=content)
-
-
-async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets,
- page_info, page, view) -> str:
- """OOB response: page-scoped events (htmx nav)."""
- from quart import url_for
- from shared.utils import route_prefix
- from shared.infrastructure.urls import events_url
-
- prefix = route_prefix()
- post = ctx.get("post") or {}
- ticket_url = url_for("page_summary.adjust_ticket")
- next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
-
- content = await _events_main_panel_html(
- ctx, entries, has_more, pending_tickets, page_info,
- page, view, ticket_url, next_url, events_url,
- is_page_scoped=True, post=post,
- )
-
- oobs = await _post_header_sx(ctx, oob=True)
- oobs += _clear_deeper_oob("post-row", "post-header-child")
- return await oob_page_sx(oobs=oobs, content=content)
-
-
-async def render_page_summary_cards(entries, has_more, pending_tickets,
- page_info, page, view, post) -> str:
- """Pagination fragment: page-scoped events cards only."""
- from quart import url_for
- from shared.utils import route_prefix
- from shared.infrastructure.urls import events_url
-
- prefix = route_prefix()
- ticket_url = url_for("page_summary.adjust_ticket")
- next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
-
- return await _entry_cards_html(
- entries, page_info, pending_tickets, ticket_url, events_url,
- view, page, has_more, next_url,
- is_page_scoped=True, post=post,
- )
-
-
-# ---------------------------------------------------------------------------
-# Calendars home
-# ---------------------------------------------------------------------------
-
-async def render_calendars_page(ctx: dict) -> str:
- """Full page: calendars listing."""
- content = await _calendars_main_panel_sx(ctx)
- ctx = await _ensure_container_nav(ctx)
- slug = (ctx.get("post") or {}).get("slug", "")
- root_hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
- post_hdr = await _post_header_sx(ctx)
- admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
- return await full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content)
-
-
-async def render_calendars_oob(ctx: dict) -> str:
- """OOB response: calendars listing."""
- content = await _calendars_main_panel_sx(ctx)
- 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")
- return await oob_page_sx(oobs=oobs, content=content)
-
-
-# ---------------------------------------------------------------------------
-# Calendar month view
-# ---------------------------------------------------------------------------
-
-async def render_calendar_page(ctx: dict) -> str:
- """Full page: calendar month view."""
- content = await _calendar_main_panel_html(ctx)
- hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
- child = await _post_header_sx(ctx) + await _calendar_header_sx(ctx)
- hdr += await header_child_sx(child)
- return await full_page_sx(ctx, header_rows=hdr, content=content)
-
-
-async def render_calendar_oob(ctx: dict) -> str:
- """OOB response: calendar month view."""
- content = await _calendar_main_panel_html(ctx)
- oobs = await _post_header_sx(ctx, oob=True)
- oobs += await oob_header_sx("post-header-child", "calendar-header-child",
- await _calendar_header_sx(ctx))
- oobs += _clear_deeper_oob("post-row", "post-header-child",
- "calendar-row", "calendar-header-child")
- return await oob_page_sx(oobs=oobs, content=content)
-
-
-# ---------------------------------------------------------------------------
-# Day detail
-# ---------------------------------------------------------------------------
-
-async def render_day_page(ctx: dict) -> str:
- """Full page: day detail."""
- content = await _day_main_panel_html(ctx)
- hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
- child = (await _post_header_sx(ctx)
- + await _calendar_header_sx(ctx) + await _day_header_sx(ctx))
- hdr += await header_child_sx(child)
- return await full_page_sx(ctx, header_rows=hdr, content=content)
-
-
-async def render_day_oob(ctx: dict) -> str:
- """OOB response: day detail."""
- content = await _day_main_panel_html(ctx)
- oobs = await _calendar_header_sx(ctx, oob=True)
- oobs += await oob_header_sx("calendar-header-child", "day-header-child",
- await _day_header_sx(ctx))
- oobs += _clear_deeper_oob("post-row", "post-header-child",
- "calendar-row", "calendar-header-child",
- "day-row", "day-header-child")
- return await oob_page_sx(oobs=oobs, content=content)
-
-
-# ---------------------------------------------------------------------------
-# Calendar admin helper
-# ---------------------------------------------------------------------------
-
-async def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
- selected: str = "") -> str:
- """Post-level admin row for events — delegates to shared helper."""
- slug = (ctx.get("post") or {}).get("slug", "")
- return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
-
-
-# ===========================================================================
-# POST / PUT / DELETE response components
-# ===========================================================================
-
-
-# ---------------------------------------------------------------------------
-# Ticket widget (public wrapper for _ticket_widget_html)
-# ---------------------------------------------------------------------------
-
-async def render_ticket_widget(entry, qty: int, ticket_url: str) -> str:
- """Render the +/- ticket widget for page_summary / all_events adjust_ticket."""
- return await _ticket_widget_html(entry, qty, ticket_url, ctx={})
-
-
-# ---------------------------------------------------------------------------
-# Ticket admin: checkin result
-# ---------------------------------------------------------------------------
-
-async def render_checkin_result(success: bool, error: str | None, ticket) -> str:
- """Render checkin result: table row on success, error div on failure."""
- if not success:
- return await render_to_sx("events-checkin-error",
- message=error or "Check-in failed")
- if not ticket:
- return ""
- code = ticket.code
- 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 = await render_to_sx("events-ticket-admin-date",
- date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))
-
- return await render_to_sx("events-checkin-success-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=SxExpr(await _ticket_state_badge_html("checked_in")),
- time_str=time_str)
-
-
-# ---------------------------------------------------------------------------
-# Ticket admin: lookup result
-# ---------------------------------------------------------------------------
-
-async def render_lookup_result(ticket, error: str | None) -> str:
- """Render ticket lookup result: error div or ticket info card."""
- from quart import url_for
- from shared.browser.app.csrf import generate_csrf_token
-
- if error:
- return await render_to_sx("events-lookup-error", message=error)
- if not ticket:
- return ""
-
- 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)
- csrf = generate_csrf_token()
-
- # Info section
- info_html = await render_to_sx("events-lookup-info",
- entry_name=entry.name if entry else "Unknown event")
- if tt:
- info_html += await render_to_sx("events-lookup-type", type_name=tt.name)
- if entry and entry.start_at:
- info_html += await render_to_sx("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 += await render_to_sx("events-lookup-cal", cal_name=cal.name)
- info_html += await render_to_sx("events-lookup-status",
- badge=SxExpr(await _ticket_state_badge_html(state)), code=code)
- if checked_in_at:
- info_html += await render_to_sx("events-lookup-checkin-time",
- date_str=checked_in_at.strftime("%B %d, %Y at %H:%M"))
-
- # Action area
- action_html = ""
- if state in ("confirmed", "reserved"):
- checkin_url = url_for("ticket_admin.do_checkin", code=code)
- action_html = await render_to_sx("events-lookup-checkin-btn",
- checkin_url=checkin_url, code=code, csrf=csrf)
- elif state == "checked_in":
- action_html = await render_to_sx("events-lookup-checked-in")
- elif state == "cancelled":
- action_html = await render_to_sx("events-lookup-cancelled")
-
- return await render_to_sx("events-lookup-card",
- info=SxExpr(info_html), code=code, action=SxExpr(action_html))
-
-
-# ---------------------------------------------------------------------------
-# Ticket admin: entry tickets table
-# ---------------------------------------------------------------------------
-
-async def render_entry_tickets_admin(entry, tickets: list) -> str:
- """Render admin ticket table for a specific entry."""
- from quart import url_for
- from shared.browser.app.csrf import generate_csrf_token
- csrf = generate_csrf_token()
-
- count = len(tickets)
- suffix = "s" if count != 1 else ""
-
- rows_html = ""
- 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 = ""
- if state in ("confirmed", "reserved"):
- checkin_url = url_for("ticket_admin.do_checkin", code=code)
- action_html = await render_to_sx("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 = await render_to_sx("events-ticket-admin-checked-in",
- time_str=t_str)
-
- rows_html += await render_to_sx("events-entry-tickets-admin-row",
- code=code, code_short=code[:12] + "...",
- type_name=tt.name if tt else "\u2014",
- badge=SxExpr(await _ticket_state_badge_html(state)),
- action=SxExpr(action_html))
-
- if tickets:
- body_html = await render_to_sx("events-entry-tickets-admin-table",
- rows=SxExpr(rows_html))
- else:
- body_html = await render_to_sx("events-entry-tickets-admin-empty")
-
- return await render_to_sx("events-entry-tickets-admin-panel",
- entry_name=entry.name,
- count_label=f"{count} ticket{suffix}",
- body=SxExpr(body_html))
-
-
-# ---------------------------------------------------------------------------
-# Day main panel -- public API
-# ---------------------------------------------------------------------------
-
-async def render_day_main_panel(ctx: dict) -> str:
- """Public wrapper for day main panel rendering."""
- return await _day_main_panel_html(ctx)
-
-
-# ---------------------------------------------------------------------------
-# Entry main panel
-# ---------------------------------------------------------------------------
-
-async def _entry_main_panel_html(ctx: dict) -> str:
- """Render the entry detail panel (name, slot, time, state, cost, tickets,
- buy form, date, posts, options + edit button)."""
- from quart import url_for
-
- entry = ctx.get("entry")
- if not entry:
- return ""
-
- calendar = ctx.get("calendar")
- cal_slug = getattr(calendar, "slug", "") if calendar else ""
- day = ctx.get("day")
- month = ctx.get("month")
- year = ctx.get("year")
- styles = ctx.get("styles") or {}
- list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
- pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
-
- eid = entry.id
- state = getattr(entry, "state", "pending") or "pending"
-
- async def _field(label, content_html):
- return await render_to_sx("events-entry-field", label=label, content=SxExpr(content_html))
-
- # Name
- name_html = await _field("Name", await render_to_sx("events-entry-name-field", name=entry.name))
-
- # Slot
- slot = getattr(entry, "slot", None)
- if slot:
- flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)"
- slot_inner = await render_to_sx("events-entry-slot-assigned",
- slot_name=slot.name, flex_label=flex_label)
- else:
- slot_inner = await render_to_sx("events-entry-slot-none")
- slot_html = await _field("Slot", slot_inner)
-
- # Time Period
- start_str = entry.start_at.strftime("%H:%M") if entry.start_at else ""
- end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended"
- time_html = await _field("Time Period", await render_to_sx("events-entry-time-field",
- time_str=start_str + end_str))
-
- # State
- state_html = await _field("State", await render_to_sx("events-entry-state-field",
- entry_id=str(eid),
- badge=SxExpr(await _entry_state_badge_html(state))))
-
- # Cost
- cost = getattr(entry, "cost", None)
- cost_str = f"{cost:.2f}" if cost is not None else "0.00"
- cost_html = await _field("Cost", await render_to_sx("events-entry-cost-field",
- cost=f"£{cost_str}"))
-
- # Ticket Configuration (admin)
- tickets_html = await _field("Tickets", await render_to_sx("events-entry-tickets-field",
- entry_id=str(eid),
- tickets_config=SxExpr(await render_entry_tickets_config(entry, calendar, day, month, year))))
-
- # Buy Tickets (public-facing)
- ticket_remaining = ctx.get("ticket_remaining")
- ticket_sold_count = ctx.get("ticket_sold_count", 0)
- user_ticket_count = ctx.get("user_ticket_count", 0)
- user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {}
- buy_html = await render_buy_form(
- entry, ticket_remaining, ticket_sold_count,
- user_ticket_count, user_ticket_counts_by_type,
- )
-
- # Date
- date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
- date_html = await _field("Date", await render_to_sx("events-entry-date-field", date_str=date_str))
-
- # Associated Posts
- entry_posts = ctx.get("entry_posts") or []
- posts_html = await _field("Associated Posts", await render_to_sx("events-entry-posts-field",
- entry_id=str(eid),
- posts_panel=SxExpr(await render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))))
-
- # Options and Edit Button
- edit_url = url_for(
- "calendar.day.calendar_entries.calendar_entry.get_edit",
- entry_id=eid, calendar_slug=cal_slug,
- day=day, month=month, year=year,
- )
-
- return await render_to_sx("events-entry-panel",
- entry_id=str(eid), list_container=list_container,
- name=SxExpr(name_html), slot=SxExpr(slot_html),
- time=SxExpr(time_html), state=SxExpr(state_html),
- cost=SxExpr(cost_html), tickets=SxExpr(tickets_html),
- buy=SxExpr(buy_html), date=SxExpr(date_html),
- posts=SxExpr(posts_html),
- options=SxExpr(await _entry_options_html(entry, calendar, day, month, year)),
- pre_action=pre_action, edit_url=edit_url)
-
-
-# ---------------------------------------------------------------------------
-# Entry header row
-# ---------------------------------------------------------------------------
-
-async def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
- """Build entry detail header row."""
- from quart import url_for
-
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- entry = ctx.get("entry")
- if not entry:
- return ""
- day = ctx.get("day")
- month = ctx.get("month")
- year = ctx.get("year")
-
- link_href = url_for(
- "calendar.day.calendar_entries.calendar_entry.get",
- calendar_slug=cal_slug,
- year=year, month=month, day=day,
- entry_id=entry.id,
- )
- label_html = await render_to_sx("events-entry-label",
- entry_id=str(entry.id),
- title=SxExpr(await _entry_title_html(entry)),
- times=SxExpr(await _entry_times_html(entry)))
-
- nav_html = await _entry_nav_html(ctx)
-
- return await render_to_sx("menu-row-sx", id="entry-row", level=5,
- link_href=link_href, link_label_content=SxExpr(label_html),
- nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob)
-
-
-async def _entry_times_html(entry) -> str:
- """Render entry times label."""
- start = entry.start_at
- end = entry.end_at
- if not start:
- return ""
- start_str = start.strftime("%H:%M")
- end_str = f" \u2192 {end.strftime('%H:%M')}" if end else ""
- return await render_to_sx("events-entry-times", time_str=start_str + end_str)
-
-
-# ---------------------------------------------------------------------------
-# Entry nav (desktop + admin link)
-# ---------------------------------------------------------------------------
-
-async def _entry_nav_html(ctx: dict) -> str:
- """Entry desktop nav: associated posts scrolling menu + admin link."""
- from quart import url_for
-
- calendar = ctx.get("calendar")
- if not calendar:
- return ""
- cal_slug = getattr(calendar, "slug", "")
- entry = ctx.get("entry")
- if not entry:
- return ""
- day = ctx.get("day")
- month = ctx.get("month")
- year = ctx.get("year")
- 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
- 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 = await render_to_sx("events-post-img", src=feat, alt=title)
- else:
- img_html = await render_to_sx("events-post-img-placeholder")
- post_links += await render_to_sx("events-entry-nav-post-link",
- href=href, img=SxExpr(img_html), title=title)
- parts.append((await render_to_sx("events-entry-posts-nav-oob",
- items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', ''))
-
- # Admin link
- if is_admin:
- admin_url = url_for(
- "calendar.day.calendar_entries.calendar_entry.admin.admin",
- calendar_slug=cal_slug,
- day=day, month=month, year=year,
- entry_id=entry.id,
- )
- parts.append(await render_to_sx("events-entry-admin-link", href=admin_url))
-
- return "".join(parts)
-
-
-# ---------------------------------------------------------------------------
-# Entry optioned (confirm/decline/provisional response)
-# ---------------------------------------------------------------------------
-
-async def render_entry_optioned(entry, calendar, day, month, year) -> str:
- """Render entry options buttons + OOB title & state swaps."""
- options = await _entry_options_html(entry, calendar, day, month, year)
- title = await _entry_title_html(entry)
- state = await _entry_state_badge_html(getattr(entry, "state", "pending") or "pending")
-
- return options + await render_to_sx("events-entry-optioned-oob",
- entry_id=str(entry.id),
- title=SxExpr(title), state=SxExpr(state))
-
-
-async def _entry_title_html(entry) -> str:
- """Render entry title (icon + name + state badge)."""
- state = getattr(entry, "state", "pending") or "pending"
- return await render_to_sx("events-entry-title",
- name=entry.name,
- badge=SxExpr(await _entry_state_badge_html(state)))
-
-
-async def _entry_options_html(entry, calendar, day, month, year) -> str:
- """Render confirm/decline/provisional buttons based on entry state."""
- from quart import url_for, g
- from shared.browser.app.csrf import generate_csrf_token
- csrf = generate_csrf_token()
-
- styles = getattr(g, "styles", None) or {}
- action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
-
- cal_slug = getattr(calendar, "slug", "")
- eid = entry.id
- state = getattr(entry, "state", "pending") or "pending"
- target = f"#calendar_entry_options_{eid}"
-
- async 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 await render_to_sx("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")
-
- buttons_html = ""
- if state == "provisional":
- buttons_html += await _make_button(
- "confirm_entry", "confirm",
- "Confirm entry?", "Are you sure you want to confirm this entry?",
- )
- buttons_html += await _make_button(
- "decline_entry", "decline",
- "Decline entry?", "Are you sure you want to decline this entry?",
- )
- elif state == "confirmed":
- buttons_html += await _make_button(
- "provisional_entry", "provisional",
- "Provisional entry?", "Are you sure you want to provisional this entry?",
- trigger_type="button",
- )
-
- return await render_to_sx("events-entry-options",
- entry_id=str(eid), buttons=SxExpr(buttons_html))
-
-
-# ---------------------------------------------------------------------------
-# Entry tickets config (display + form)
-# ---------------------------------------------------------------------------
-
-async def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
- """Render ticket config display + edit form for admin entry view."""
- 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
- tp = getattr(entry, "ticket_price", None)
- tc = getattr(entry, "ticket_count", None)
- eid_s = str(eid)
- show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');"
- hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); "
- f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));")
-
- if tp is not None:
- tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
- display_html = await render_to_sx("events-ticket-config-display",
- price_str=f"£{tp:.2f}",
- count_str=tc_str, show_js=show_js)
- else:
- display_html = await render_to_sx("events-ticket-config-none", show_js=show_js)
-
- update_url = url_for(
- "calendar.day.calendar_entries.calendar_entry.update_tickets",
- entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year,
- )
- hidden_cls = "" if tp is None else "hidden"
- tp_val = f"{tp:.2f}" if tp is not None else ""
- tc_val = str(tc) if tc is not None else ""
-
- form_html = await render_to_sx("events-ticket-config-form",
- entry_id=eid_s, hidden_cls=hidden_cls,
- update_url=update_url, csrf=csrf,
- price_val=tp_val, count_val=tc_val, hide_js=hide_js)
- return display_html + form_html
-
-
-# ---------------------------------------------------------------------------
-# Entry posts panel
-# ---------------------------------------------------------------------------
-
-async def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
- """Render associated posts list with remove buttons and search input."""
- 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
- eid_s = str(eid)
-
- posts_html = ""
- 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 = (await render_to_sx("events-post-img", src=feat, alt=ep_title)
- if feat else await render_to_sx("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 += await render_to_sx("events-entry-post-item",
- img=SxExpr(img_html), title=ep_title,
- del_url=del_url, entry_id=eid_s,
- csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
- posts_html = await render_to_sx("events-entry-posts-list", items=SxExpr(items))
- else:
- posts_html = await render_to_sx("events-entry-posts-none")
-
- 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 await render_to_sx("events-entry-posts-panel",
- posts=SxExpr(posts_html), search_url=search_url,
- entry_id=eid_s)
-
-
-# ---------------------------------------------------------------------------
-# Entry posts nav OOB
-# ---------------------------------------------------------------------------
-
-async def render_entry_posts_nav_oob(entry_posts) -> str:
- """Render OOB nav for entry posts (scrolling menu)."""
- 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 await render_to_sx("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 = (await render_to_sx("events-post-img", src=feat, alt=title)
- if feat else await render_to_sx("events-post-img-placeholder"))
- items += await render_to_sx("events-entry-nav-post",
- href=href, nav_btn=nav_btn,
- img=SxExpr(img_html), title=title)
-
- return await render_to_sx("events-entry-posts-nav-oob", items=SxExpr(items))
-
-
-# ---------------------------------------------------------------------------
-# Day entries nav OOB
-# ---------------------------------------------------------------------------
-
-async def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
- """Render OOB nav for confirmed entries in a day."""
- 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 await render_to_sx("events-day-entries-nav-oob-empty")
-
- items = ""
- for entry in confirmed_entries:
- href = url_for(
- "calendar.day.calendar_entries.calendar_entry.get",
- 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 += await render_to_sx("events-day-nav-entry",
- href=href, nav_btn=nav_btn,
- name=entry.name, time_str=start + end)
-
- return await render_to_sx("events-day-entries-nav-oob", items=SxExpr(items))
-
-
-# ---------------------------------------------------------------------------
-# Post nav entries OOB
-# ---------------------------------------------------------------------------
-
-async def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
- """Render OOB nav for associated entries and calendars of a post."""
- from quart import g
- from shared.infrastructure.urls import events_url
-
- styles = getattr(g, "styles", None) or {}
- 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 await render_to_sx("events-post-nav-oob-empty")
-
- slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")
-
- items = ""
- if has_entries:
- for entry in associated_entries.entries:
- entry_path = (
- f"/{slug}/{entry.calendar_slug}/"
- 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 += await render_to_sx("events-post-nav-entry",
- href=href, nav_btn=nav_btn,
- name=entry.name, time_str=time_str + end_str)
-
- if calendars:
- for cal in calendars:
- cs = getattr(cal, "slug", "")
- local_href = events_url(f"/{slug}/{cs}/")
- items += await render_to_sx("events-post-nav-calendar",
- href=local_href, nav_btn=nav_btn, 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 await render_to_sx("events-post-nav-wrapper",
- items=SxExpr(items), hyperscript=hs)
-
-
-# ---------------------------------------------------------------------------
-# Calendar description display + edit form
-# ---------------------------------------------------------------------------
-
-async def render_calendar_description(calendar, *, oob: bool = False) -> str:
- """Render calendar description display with edit button, optionally with OOB title."""
- from quart import url_for
-
- cal_slug = getattr(calendar, "slug", "")
- edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
- html = await _calendar_description_display_html(calendar, edit_url)
-
- if oob:
- desc = getattr(calendar, "description", "") or ""
- html += await render_to_sx("events-calendar-description-title-oob",
- description=desc)
- return html
-
-
-async def render_calendar_description_edit(calendar) -> str:
- """Render calendar description edit form."""
- from quart import url_for
- from shared.browser.app.csrf import generate_csrf_token
- csrf = generate_csrf_token()
- cal_slug = getattr(calendar, "slug", "")
- desc = getattr(calendar, "description", "") or ""
-
- save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug)
- cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug)
-
- return await render_to_sx("events-calendar-description-edit-form",
- save_url=save_url, cancel_url=cancel_url,
- csrf=csrf, description=desc)
-
-
-# ---------------------------------------------------------------------------
-# Calendars list panel (for POST create / DELETE)
-# ---------------------------------------------------------------------------
-
-async def render_calendars_list_panel(ctx: dict) -> str:
- """Render the calendars main panel HTML for POST/DELETE response."""
- return await _calendars_main_panel_sx(ctx)
-
-
-# ---------------------------------------------------------------------------
-# Markets list panel (for POST create / DELETE)
-# ---------------------------------------------------------------------------
-
-async def render_markets_list_panel(ctx: dict) -> str:
- """Render the markets main panel HTML for POST/DELETE response."""
- return await _markets_main_panel_html(ctx)
-
-
-# ---------------------------------------------------------------------------
-# Slot main panel
-# ---------------------------------------------------------------------------
-
-async def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
- """Render slot detail view."""
- from quart import url_for, g
-
- styles = getattr(g, "styles", None) or {}
- list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
- pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
- cal_slug = getattr(calendar, "slug", "")
-
- days_display = getattr(slot, "days_display", "\u2014")
- days = days_display.split(", ")
- flexible = getattr(slot, "flexible", False)
- 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(
- await render_to_sx("events-slot-day-pill", day=d) for d in days
- )
- days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner))
- else:
- days_html = await render_to_sx("events-slot-no-days")
-
- sid = str(slot.id)
-
- result = await render_to_sx("events-slot-panel",
- slot_id=sid, list_container=list_container,
- days=SxExpr(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 += await render_to_sx("events-slot-description-oob", description=desc)
-
- return result
-
-
-# ---------------------------------------------------------------------------
-# Slots table
-# ---------------------------------------------------------------------------
-
-async def render_slots_table(slots, calendar) -> str:
- """Render slots table with rows and add button."""
- from quart import url_for, g
- from shared.browser.app.csrf import generate_csrf_token
- csrf = generate_csrf_token()
-
- styles = getattr(g, "styles", None) or {}
- list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
- tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
- pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
- action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
- pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
- hx_select = getattr(g, "hx_select_search", "#main-panel")
- cal_slug = getattr(calendar, "slug", "")
-
- rows_html = ""
- 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(
- await render_to_sx("events-slot-day-pill", day=d) for d in day_list
- )
- days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner))
- else:
- days_html = await render_to_sx("events-slot-no-days")
-
- 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 ""
-
- rows_html += await render_to_sx("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=SxExpr(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 = await render_to_sx("events-slots-empty-row")
-
- add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
-
- return await render_to_sx("events-slots-table",
- list_container=list_container, rows=SxExpr(rows_html),
- pre_action=pre_action, add_url=add_url)
-
-
-# ---------------------------------------------------------------------------
-# Ticket type main panel
-# ---------------------------------------------------------------------------
-
-async def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str:
- """Render ticket type detail view."""
- from quart import url_for, g
-
- styles = getattr(g, "styles", None) or {}
- list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
- pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
- cal_slug = getattr(calendar, "slug", "")
-
- cost = getattr(ticket_type, "cost", None)
- cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
- count = getattr(ticket_type, "count", 0)
- tid = str(ticket_type.id)
-
- edit_url = url_for(
- "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit",
- ticket_type_id=ticket_type.id, calendar_slug=cal_slug,
- year=year, month=month, day=day, entry_id=entry.id,
- )
-
- async def _col(label, val):
- return await render_to_sx("events-ticket-type-col", label=label, value=val)
-
- return await render_to_sx("events-ticket-type-panel",
- ticket_id=tid, list_container=list_container,
- c1=await _col("Name", ticket_type.name),
- c2=await _col("Cost", cost_str),
- c3=await _col("Count", str(count)),
- pre_action=pre_action, edit_url=edit_url)
-
-
-# ---------------------------------------------------------------------------
-# Ticket types table
-# ---------------------------------------------------------------------------
-
-async def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str:
- """Render ticket types table with rows and add button."""
- from quart import url_for, g
- from shared.browser.app.csrf import generate_csrf_token
- csrf = generate_csrf_token()
-
- styles = getattr(g, "styles", None) or {}
- list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
- tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
- pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
- action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
- hx_select = getattr(g, "hx_select_search", "#main-panel")
- cal_slug = getattr(calendar, "slug", "")
- eid = entry.id
-
- rows_html = ""
- 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 += await render_to_sx("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 = await render_to_sx("events-ticket-types-empty-row")
-
- 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 await render_to_sx("events-ticket-types-table",
- list_container=list_container, rows=SxExpr(rows_html),
- action_btn=action_btn, add_url=add_url)
-
-
-# ---------------------------------------------------------------------------
-# Buy result (ticket purchase confirmation)
-# ---------------------------------------------------------------------------
-
-async def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
- """Render buy result card with created tickets + OOB cart icon."""
- from quart import url_for
-
- cart_html = await _cart_icon_oob(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 += await render_to_sx("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 = await render_to_sx("events-buy-result-remaining",
- text=f"{remaining} ticket{r_suffix} remaining")
-
- my_href = url_for("defpage_my_tickets")
-
- return cart_html + await render_to_sx("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)
-
-
-# ---------------------------------------------------------------------------
-# Buy form (ticket +/- controls)
-# ---------------------------------------------------------------------------
-
-async 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."""
- 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 await render_to_sx("events-buy-not-confirmed", entry_id=eid_s)
-
- adjust_url = url_for("tickets.adjust_quantity")
- target = f"#ticket-buy-{eid}"
-
- # Info line
- info_html = ""
- info_items = ""
- if ticket_sold_count:
- info_items += await render_to_sx("events-buy-info-sold",
- count=str(ticket_sold_count))
- if ticket_remaining is not None:
- info_items += await render_to_sx("events-buy-info-remaining",
- count=str(ticket_remaining))
- if user_ticket_count:
- info_items += await render_to_sx("events-buy-info-basket",
- count=str(user_ticket_count))
- if info_items:
- info_html = await render_to_sx("events-buy-info-bar", items=SxExpr(info_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 += await render_to_sx("events-buy-type-item",
- type_name=tt.name, cost_str=cost_str,
- adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)))
- body_html = await render_to_sx("events-buy-types-wrapper", items=SxExpr(type_items))
- else:
- qty = user_ticket_count or 0
- body_html = await render_to_sx("events-buy-default",
- price_str=f"\u00a3{tp:.2f}",
- adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, qty)))
-
- return await render_to_sx("events-buy-panel",
- entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html))
-
-
-async 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 = await render_to_sx("events-adjust-tt-hidden",
- ticket_type_id=str(ticket_type_id)) if ticket_type_id else ""
- eid_s = str(entry_id)
-
- async def _adj_form(count_val, btn_html, *, extra_cls=""):
- return await render_to_sx("events-adjust-form",
- adjust_url=adjust_url, target=target,
- extra_cls=extra_cls, csrf=csrf,
- entry_id=eid_s, tt=SxExpr(tt_html) if tt_html else None,
- count_val=str(count_val), btn=SxExpr(btn_html))
-
- if count == 0:
- return await _adj_form(1, await render_to_sx("events-adjust-cart-plus"),
- extra_cls="flex items-center")
-
- my_tickets_href = url_for("defpage_my_tickets")
- minus = await _adj_form(count - 1, await render_to_sx("events-adjust-minus"))
- cart_icon = await render_to_sx("events-adjust-cart-icon",
- href=my_tickets_href, count=str(count))
- plus = await _adj_form(count + 1, await render_to_sx("events-adjust-plus"))
-
- return await render_to_sx("events-adjust-controls",
- minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus))
-
-
-# ---------------------------------------------------------------------------
-# Adjust response (OOB cart icon + buy form)
-# ---------------------------------------------------------------------------
-
-async 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 = await _cart_icon_oob(cart_count)
- form_html = await render_buy_form(
- entry, ticket_remaining, ticket_sold_count,
- user_ticket_count, user_ticket_counts_by_type,
- )
- return cart_html + form_html
-
-
-async def _cart_icon_oob(count: int) -> str:
- """Render the OOB cart icon/badge swap."""
- from quart import g
-
- blog_url_fn = getattr(g, "blog_url", None)
- cart_url_fn = getattr(g, "cart_url", None)
- site_fn = getattr(g, "site", None)
- logo = ""
- if site_fn:
- 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 await render_to_sx("events-cart-icon-logo",
- blog_href=blog_href, logo=logo)
-
- cart_href = cart_url_fn("/") if cart_url_fn else "/"
- return await render_to_sx("events-cart-icon-badge",
- cart_href=cart_href, count=str(count))
-
-
-# ===========================================================================
-# SLOT PICKER JS — shared by entry edit + entry add forms
-# ===========================================================================
-
-_SLOT_PICKER_JS = """\
-"""
-
-
-# ===========================================================================
-# Entry edit form
-# ===========================================================================
-
-async def _slot_options_html(day_slots, selected_slot_id=None) -> str:
- """Build slot