diff --git a/account/bp/account/routes.py b/account/bp/account/routes.py index 835353f..c4d2ce0 100644 --- a/account/bp/account/routes.py +++ b/account/bp/account/routes.py @@ -56,6 +56,6 @@ def register(url_prefix="/"): await g.s.flush() from sx.sx_components import render_newsletter_toggle - return sx_response(render_newsletter_toggle(un)) + return sx_response(await render_newsletter_toggle(un)) return account_bp diff --git a/account/sx/sx_components.py b/account/sx/sx_components.py index a094a5e..9073111 100644 --- a/account/sx/sx_components.py +++ b/account/sx/sx_components.py @@ -11,7 +11,7 @@ from typing import Any from shared.sx.jinja_bridge import load_service_components from shared.sx.helpers import ( - sx_call, SxExpr, + render_to_sx, root_header_sx, full_page_sx, ) @@ -28,9 +28,10 @@ async def render_login_page(ctx: dict) -> str: """Full page: login form.""" error = ctx.get("error", "") email = ctx.get("email", "") - hdr = root_header_sx(ctx) - content = sx_call("account-login-content", error=error or None, email=email) - return full_page_sx(ctx, header_rows=hdr, + hdr = await root_header_sx(ctx) + content = await render_to_sx("account-login-content", + error=error or None, email=email) + return await full_page_sx(ctx, header_rows=hdr, content=content, meta_html='Login \u2014 Rose Ash') @@ -39,18 +40,19 @@ async def render_device_page(ctx: dict) -> str: """Full page: device authorization form.""" error = ctx.get("error", "") code = ctx.get("code", "") - hdr = root_header_sx(ctx) - content = sx_call("account-device-content", error=error or None, code=code) - return full_page_sx(ctx, header_rows=hdr, + hdr = await root_header_sx(ctx) + content = await render_to_sx("account-device-content", + error=error or None, code=code) + return await full_page_sx(ctx, header_rows=hdr, content=content, meta_html='Authorize Device \u2014 Rose Ash') async def render_device_approved_page(ctx: dict) -> str: """Full page: device approved.""" - hdr = root_header_sx(ctx) - content = sx_call("account-device-approved") - return full_page_sx(ctx, header_rows=hdr, + hdr = await root_header_sx(ctx) + content = await render_to_sx("account-device-approved") + return await full_page_sx(ctx, header_rows=hdr, content=content, meta_html='Device Authorized \u2014 Rose Ash') @@ -59,10 +61,10 @@ async def render_check_email_page(ctx: dict) -> str: """Full page: check email after magic link sent.""" email = ctx.get("email", "") email_error = ctx.get("email_error") - hdr = root_header_sx(ctx) - content = sx_call("account-check-email-content", - email=email, email_error=email_error) - return full_page_sx(ctx, header_rows=hdr, + hdr = await root_header_sx(ctx) + content = await render_to_sx("account-check-email-content", + email=email, email_error=email_error) + return await full_page_sx(ctx, header_rows=hdr, content=content, meta_html='Check your email \u2014 Rose Ash') @@ -71,7 +73,7 @@ async def render_check_email_page(ctx: dict) -> str: # Public API: Fragment renderers for POST handlers # --------------------------------------------------------------------------- -def render_newsletter_toggle(un) -> str: +async def render_newsletter_toggle(un) -> str: """Render a newsletter toggle switch for POST response.""" from shared.browser.app.csrf import generate_csrf_token @@ -94,7 +96,7 @@ def render_newsletter_toggle(un) -> str: translate = "translate-x-1" checked = "false" - return sx_call( + return await render_to_sx( "account-newsletter-toggle", id=f"nl-{nid}", url=toggle_url, hdrs=f'{{"X-CSRFToken": "{csrf}"}}', @@ -103,27 +105,3 @@ def render_newsletter_toggle(un) -> str: checked=checked, knob_cls=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}", ) - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- - -def _fragment_content(frag: object) -> str: - """Convert a fragment response to sx content string. - - SxExpr (from text/sx responses) is embedded as-is; plain strings - (from text/html) are wrapped in ``~rich-text``. - """ - from shared.sx.parser import SxExpr - if isinstance(frag, SxExpr): - return frag.source - s = str(frag) if frag else "" - if not s: - return "" - return f'(~rich-text :html "{_sx_escape(s)}")' - - -def _sx_escape(s: str) -> str: - """Escape a string for embedding in sx string literals.""" - return s.replace("\\", "\\\\").replace('"', '\\"') diff --git a/account/sxc/pages/__init__.py b/account/sxc/pages/__init__.py index 12aebc4..3624fb7 100644 --- a/account/sxc/pages/__init__.py +++ b/account/sxc/pages/__init__.py @@ -26,43 +26,51 @@ def _register_account_layouts() -> None: register_custom_layout("account", _account_full, _account_oob, _account_mobile) -def _account_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, header_child_sx, call_url, sx_call, SxExpr +async def _account_full(ctx: dict, **kw: Any) -> str: + from shared.sx.helpers import root_header_sx, header_child_sx, render_to_sx - root_hdr = root_header_sx(ctx) - auth_hdr = sx_call("auth-header-row", - account_url=call_url(ctx, "account_url", ""), + root_hdr = await root_header_sx(ctx) + auth_hdr = await render_to_sx("auth-header-row", + account_url=_call_url(ctx, "account_url", ""), select_colours=ctx.get("select_colours", ""), account_nav=_as_sx_nav(ctx), ) - hdr_child = header_child_sx(auth_hdr) + hdr_child = await header_child_sx(auth_hdr) return "(<> " + root_hdr + " " + hdr_child + ")" -def _account_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, call_url, sx_call +async def _account_oob(ctx: dict, **kw: Any) -> str: + from shared.sx.helpers import root_header_sx, render_to_sx - auth_hdr = sx_call("auth-header-row", - account_url=call_url(ctx, "account_url", ""), + auth_hdr = await render_to_sx("auth-header-row", + account_url=_call_url(ctx, "account_url", ""), select_colours=ctx.get("select_colours", ""), account_nav=_as_sx_nav(ctx), oob=True, ) - return "(<> " + auth_hdr + " " + root_header_sx(ctx, oob=True) + ")" + return "(<> " + auth_hdr + " " + await root_header_sx(ctx, oob=True) + ")" -def _account_mobile(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr, call_url +async def _account_mobile(ctx: dict, **kw: Any) -> str: + from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx, render_to_sx + from shared.sx.parser import SxExpr ctx = _inject_account_nav(ctx) - nav_items = sx_call("auth-nav-items", - account_url=call_url(ctx, "account_url", ""), + nav_items = await render_to_sx("auth-nav-items", + account_url=_call_url(ctx, "account_url", ""), select_colours=ctx.get("select_colours", ""), account_nav=_as_sx_nav(ctx), ) - auth_section = sx_call("mobile-menu-section", + auth_section = await render_to_sx("mobile-menu-section", label="account", href="/", level=1, colour="sky", items=SxExpr(nav_items)) - return mobile_menu_sx(auth_section, mobile_root_nav_sx(ctx)) + return mobile_menu_sx(auth_section, await mobile_root_nav_sx(ctx)) + + +def _call_url(ctx: dict, key: str, path: str = "/") -> str: + fn = ctx.get(key) + if callable(fn): + return fn(path) + return str(fn or "") + path def _inject_account_nav(ctx: dict) -> dict: @@ -75,7 +83,7 @@ def _inject_account_nav(ctx: dict) -> dict: def _as_sx_nav(ctx: dict) -> Any: - """Convert account_nav fragment to SxExpr for use in sx_call.""" + """Convert account_nav fragment to SxExpr for use in component calls.""" from shared.sx.helpers import _as_sx ctx = _inject_account_nav(ctx) return _as_sx(ctx.get("account_nav")) @@ -100,8 +108,7 @@ async def _h_newsletters_content(**kw): from sqlalchemy import select from shared.models import UserNewsletter from shared.models.ghost_membership_entities import GhostNewsletter - from shared.sx.helpers import sx_call, SxExpr - from shared.sx.parser import serialize + from shared.sx.helpers import render_to_sx result = await g.s.execute( select(GhostNewsletter).order_by(GhostNewsletter.name) @@ -131,8 +138,8 @@ async def _h_newsletters_content(**kw): # Call account_url to get the base URL string account_url_str = account_url("") if callable(account_url) else str(account_url or "") - return sx_call("account-newsletters-content", - newsletter_list=SxExpr(serialize(newsletter_list)), + return await render_to_sx("account-newsletters-content", + newsletter_list=newsletter_list, account_url=account_url_str) @@ -148,5 +155,11 @@ async def _h_fragment_content(slug=None, **kw): ) if not fragment_html: abort(404) - from sx.sx_components import _fragment_content - return _fragment_content(fragment_html) + from shared.sx.parser import SxExpr + if isinstance(fragment_html, SxExpr): + return fragment_html.source + s = str(fragment_html) if fragment_html else "" + if not s: + return "" + from shared.sx.helpers import render_to_sx + return await render_to_sx("rich-text", html=s) diff --git a/blog/bp/blog/routes.py b/blog/bp/blog/routes.py index 1a3001f..b51ee40 100644 --- a/blog/bp/blog/routes.py +++ b/blog/bp/blog/routes.py @@ -235,7 +235,7 @@ def register(url_prefix, title): from shared.sx.page import get_template_context from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() - tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.") + tctx["editor_html"] = await render_editor_panel(save_error="Invalid JSON in editor content.") html = await render_new_post_page(tctx) return await make_response(html, 400) @@ -244,7 +244,7 @@ def register(url_prefix, title): from shared.sx.page import get_template_context from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() - tctx["editor_html"] = render_editor_panel(save_error=reason) + tctx["editor_html"] = await render_editor_panel(save_error=reason) html = await render_new_post_page(tctx) return await make_response(html, 400) @@ -291,7 +291,7 @@ def register(url_prefix, title): from shared.sx.page import get_template_context from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() - tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True) + tctx["editor_html"] = await render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True) tctx["is_page"] = True html = await render_new_post_page(tctx) return await make_response(html, 400) @@ -301,7 +301,7 @@ def register(url_prefix, title): from shared.sx.page import get_template_context from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() - tctx["editor_html"] = render_editor_panel(save_error=reason, is_page=True) + tctx["editor_html"] = await render_editor_panel(save_error=reason, is_page=True) tctx["is_page"] = True html = await render_new_post_page(tctx) return await make_response(html, 400) diff --git a/blog/bp/menu_items/routes.py b/blog/bp/menu_items/routes.py index e43381d..166a81c 100644 --- a/blog/bp/menu_items/routes.py +++ b/blog/bp/menu_items/routes.py @@ -17,10 +17,10 @@ from shared.sx.helpers import sx_response def register(): bp = Blueprint("menu_items", __name__, url_prefix='/settings/menu_items') - def get_menu_items_nav_oob_sync(menu_items): + async def get_menu_items_nav_oob_async(menu_items): """Helper to generate OOB update for root nav menu items""" from sx.sx_components import render_menu_items_nav_oob - return render_menu_items_nav_oob(menu_items) + return await render_menu_items_nav_oob(menu_items) @bp.get("/new/") @require_admin @@ -51,8 +51,8 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) from sx.sx_components import render_menu_items_list - html = render_menu_items_list(menu_items) - nav_oob = get_menu_items_nav_oob_sync(menu_items) + html = await render_menu_items_list(menu_items) + nav_oob = await get_menu_items_nav_oob_async(menu_items) return sx_response(html + nav_oob) except MenuItemError as e: @@ -91,8 +91,8 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) from sx.sx_components import render_menu_items_list - html = render_menu_items_list(menu_items) - nav_oob = get_menu_items_nav_oob_sync(menu_items) + html = await render_menu_items_list(menu_items) + nav_oob = await get_menu_items_nav_oob_async(menu_items) return sx_response(html + nav_oob) except MenuItemError as e: @@ -112,8 +112,8 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) from sx.sx_components import render_menu_items_list - html = render_menu_items_list(menu_items) - nav_oob = get_menu_items_nav_oob_sync(menu_items) + html = await render_menu_items_list(menu_items) + nav_oob = await get_menu_items_nav_oob_async(menu_items) return sx_response(html + nav_oob) @bp.get("/pages/search/") @@ -128,7 +128,7 @@ def register(): has_more = (page * per_page) < total from sx.sx_components import render_page_search_results - return sx_response(render_page_search_results(pages, query, page, has_more)) + return sx_response(await render_page_search_results(pages, query, page, has_more)) @bp.post("/reorder/") @require_admin @@ -153,8 +153,8 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) from sx.sx_components import render_menu_items_list - html = render_menu_items_list(menu_items) - nav_oob = get_menu_items_nav_oob_sync(menu_items) + html = await render_menu_items_list(menu_items) + nav_oob = await get_menu_items_nav_oob_async(menu_items) return sx_response(html + nav_oob) return bp diff --git a/blog/bp/post/admin/routes.py b/blog/bp/post/admin/routes.py index 227b658..4479cd1 100644 --- a/blog/bp/post/admin/routes.py +++ b/blog/bp/post/admin/routes.py @@ -90,7 +90,7 @@ def register(): features = result.get("features", {}) from sx.sx_components import render_features_panel - html = render_features_panel( + html = await render_features_panel( features, post, sumup_configured=result.get("sumup_configured", False), sumup_merchant_code=result.get("sumup_merchant_code") or "", @@ -129,7 +129,7 @@ def register(): features = result.get("features", {}) from sx.sx_components import render_features_panel - html = render_features_panel( + html = await render_features_panel( features, post, sumup_configured=result.get("sumup_configured", False), sumup_merchant_code=result.get("sumup_merchant_code") or "", @@ -259,8 +259,8 @@ def register(): from sx.sx_components import render_associated_entries, render_nav_entries_oob post = g.post_data["post"] - admin_list = render_associated_entries(all_calendars, associated_entry_ids, post["slug"]) - nav_entries_html = render_nav_entries_oob(associated_entries, calendars, post) + admin_list = await render_associated_entries(all_calendars, associated_entry_ids, post["slug"]) + nav_entries_html = await render_nav_entries_oob(associated_entries, calendars, post) return sx_response(admin_list + nav_entries_html) @@ -436,7 +436,7 @@ def register(): page_markets = await _fetch_page_markets(post_id) from sx.sx_components import render_markets_panel - return sx_response(render_markets_panel(page_markets, post)) + return sx_response(await render_markets_panel(page_markets, post)) @bp.post("/markets/new/") @require_admin @@ -462,7 +462,7 @@ def register(): page_markets = await _fetch_page_markets(post_id) from sx.sx_components import render_markets_panel - return sx_response(render_markets_panel(page_markets, post)) + return sx_response(await render_markets_panel(page_markets, post)) @bp.delete("/markets//") @require_admin @@ -482,6 +482,6 @@ def register(): page_markets = await _fetch_page_markets(post_id) from sx.sx_components import render_markets_panel - return sx_response(render_markets_panel(page_markets, post)) + return sx_response(await render_markets_panel(page_markets, post)) return bp diff --git a/blog/bp/post/routes.py b/blog/bp/post/routes.py index 8951937..1ea2dfa 100644 --- a/blog/bp/post/routes.py +++ b/blog/bp/post/routes.py @@ -125,7 +125,7 @@ def register(): # Get post_id from g.post_data if not g.user: - return sx_response(render_like_toggle_button(slug, False, like_url), status=403) + return sx_response(await render_like_toggle_button(slug, False, like_url), status=403) post_id = g.post_data["post"]["id"] user_id = g.user.id @@ -135,7 +135,7 @@ def register(): }) liked = result["liked"] - return sx_response(render_like_toggle_button(slug, liked, like_url)) + return sx_response(await render_like_toggle_button(slug, liked, like_url)) @bp.get("/w//") async def widget_paginate(slug: str, widget_domain: str): diff --git a/blog/bp/snippets/routes.py b/blog/bp/snippets/routes.py index f64d00b..6e47d59 100644 --- a/blog/bp/snippets/routes.py +++ b/blog/bp/snippets/routes.py @@ -47,7 +47,7 @@ def register(): snippets = await _visible_snippets(g.s) from sx.sx_components import render_snippets_list - return sx_response(render_snippets_list(snippets, is_admin)) + return sx_response(await render_snippets_list(snippets, is_admin)) @bp.patch("//visibility/") @require_login @@ -71,6 +71,6 @@ def register(): snippets = await _visible_snippets(g.s) from sx.sx_components import render_snippets_list - return sx_response(render_snippets_list(snippets, True)) + return sx_response(await render_snippets_list(snippets, True)) return bp diff --git a/blog/sx/sx_components.py b/blog/sx/sx_components.py index 9f383c1..2c46b9f 100644 --- a/blog/sx/sx_components.py +++ b/blog/sx/sx_components.py @@ -13,9 +13,9 @@ from typing import Any from markupsafe import escape from shared.sx.jinja_bridge import load_service_components -from shared.sx.parser import serialize as sx_serialize +from shared.sx.parser import serialize as sx_serialize, SxExpr from shared.sx.helpers import ( - SxExpr, sx_call, + render_to_sx, call_url, get_asset_url, root_header_sx, post_header_sx, @@ -53,9 +53,9 @@ _oob_header_sx = oob_header_sx # Blog header (root-header-child -> blog-header-child) # --------------------------------------------------------------------------- -def _blog_header_sx(ctx: dict, *, oob: bool = False) -> str: +async def _blog_header_sx(ctx: dict, *, oob: bool = False) -> str: """Blog header row — empty child of root.""" - return sx_call("menu-row-sx", + return await render_to_sx("menu-row-sx", id="blog-row", level=1, link_label_content=SxExpr("(div)"), child_id="blog-header-child", oob=oob, @@ -65,28 +65,28 @@ def _blog_header_sx(ctx: dict, *, oob: bool = False) -> str: # Post header helpers — thin wrapper over shared post_header_sx # --------------------------------------------------------------------------- -def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: +async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the post-level header row as sx — delegates to shared helper.""" - return post_header_sx(ctx, oob=oob) + return await post_header_sx(ctx, oob=oob) # --------------------------------------------------------------------------- # Post admin header # --------------------------------------------------------------------------- -def _post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: +async def _post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: """Post admin header row as sx — delegates to shared helper.""" slug = (ctx.get("post") or {}).get("slug", "") - return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) + return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected) -def _post_admin_mobile_menu(ctx: dict, selected: str = "") -> str: +async def _post_admin_mobile_menu(ctx: dict, selected: str = "") -> str: """Full mobile menu for any post admin page (admin + post + root).""" slug = (ctx.get("post") or {}).get("slug", "") return mobile_menu_sx( - post_admin_mobile_nav_sx(ctx, slug, selected), - post_mobile_nav_sx(ctx), - mobile_root_nav_sx(ctx), + await post_admin_mobile_nav_sx(ctx, slug, selected), + await post_mobile_nav_sx(ctx), + await mobile_root_nav_sx(ctx), ) @@ -94,15 +94,15 @@ def _post_admin_mobile_menu(ctx: dict, selected: str = "") -> str: # Settings header (root-header-child -> root-settings-header-child) # --------------------------------------------------------------------------- -def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str: +async def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str: """Settings header row with admin icon and nav links (sx).""" from quart import url_for as qurl settings_href = qurl("settings.defpage_settings_home") - label_sx = sx_call("blog-admin-label") - nav_sx = _settings_nav_sx(ctx) + label_sx = await render_to_sx("blog-admin-label") + nav_sx = await _settings_nav_sx(ctx) - return sx_call("menu-row-sx", + return await render_to_sx("menu-row-sx", id="root-settings-row", level=1, link_href=settings_href, link_label_content=SxExpr(label_sx), @@ -112,7 +112,7 @@ def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str: -def _settings_nav_sx(ctx: dict) -> str: +async def _settings_nav_sx(ctx: dict) -> str: """Settings desktop nav as sx.""" from quart import url_for as qurl @@ -126,7 +126,7 @@ def _settings_nav_sx(ctx: dict) -> str: ("settings.defpage_cache_page", "refresh", "Cache"), ]: href = qurl(endpoint) - parts.append(sx_call("nav-link", + parts.append(await render_to_sx("nav-link", href=href, icon=f"fa fa-{icon}", label=label, select_colours=select_colours, )) @@ -139,15 +139,15 @@ def _settings_nav_sx(ctx: dict) -> str: # Sub-settings headers (root-settings-header-child -> X-header-child) # --------------------------------------------------------------------------- -def _sub_settings_header_sx(row_id: str, child_id: str, href: str, +async def _sub_settings_header_sx(row_id: str, child_id: str, href: str, icon: str, label: str, ctx: dict, *, oob: bool = False, nav_sx: str = "") -> str: """Generic sub-settings header row as sx.""" - label_sx = sx_call("blog-sub-settings-label", + label_sx = await render_to_sx("blog-sub-settings-label", icon=f"fa fa-{icon}", label=label, ) - return sx_call("menu-row-sx", + return await render_to_sx("menu-row-sx", id=row_id, level=2, link_href=href, link_label_content=SxExpr(label_sx), @@ -163,16 +163,15 @@ def _sub_settings_header_sx(row_id: str, child_id: str, href: str, -def _blog_sentinel_sx(ctx: dict) -> str: +async def _blog_sentinel_sx(ctx: dict) -> str: """Infinite scroll sentinels as sx calls (for wire format).""" - from shared.sx.helpers import sx_call page = ctx.get("page", 1) total_pages = ctx.get("total_pages", 1) if isinstance(total_pages, str): total_pages = int(total_pages) if page >= total_pages: - return sx_call("end-of-results") + return await render_to_sx("end-of-results") current_local_href = ctx.get("current_local_href", "/index") next_url = f"{current_local_href}?page={page + 1}" @@ -202,23 +201,23 @@ def _blog_sentinel_sx(ctx: dict) -> str: ) return ( - sx_call("sentinel-mobile", id=f"sentinel-{page}-m", next_url=next_url, hyperscript=mobile_hs) + await render_to_sx("sentinel-mobile", id=f"sentinel-{page}-m", next_url=next_url, hyperscript=mobile_hs) + " " - + sx_call("sentinel-desktop", id=f"sentinel-{page}-d", next_url=next_url, hyperscript=desktop_hs) + + await render_to_sx("sentinel-desktop", id=f"sentinel-{page}-d", next_url=next_url, hyperscript=desktop_hs) ) -def _blog_cards_sx(ctx: dict) -> str: +async def _blog_cards_sx(ctx: dict) -> str: """S-expression wire format for blog cards (client renders).""" posts = ctx.get("posts") or [] view = ctx.get("view") parts = [] for p in posts: if view == "tile": - parts.append(_blog_card_tile_sx(p, ctx)) + parts.append(await _blog_card_tile_sx(p, ctx)) else: - parts.append(_blog_card_sx(p, ctx)) - parts.append(_blog_sentinel_sx(ctx)) + parts.append(await _blog_card_sx(p, ctx)) + parts.append(await _blog_sentinel_sx(ctx)) return "(<> " + " ".join(parts) + ")" @@ -249,7 +248,7 @@ def _author_data(authors: list) -> list[dict]: return result -def _blog_card_sx(post: dict, ctx: dict) -> str: +async def _blog_card_sx(post: dict, ctx: dict) -> str: """Single blog card as sx call (wire format) — pure data, no HTML.""" from quart import g @@ -296,10 +295,10 @@ def _blog_card_sx(post: dict, ctx: dict) -> str: if widget: kwargs["widget"] = SxExpr(widget) if widget else None - return sx_call("blog-card", **kwargs) + return await render_to_sx("blog-card", **kwargs) -def _blog_card_tile_sx(post: dict, ctx: dict) -> str: +async def _blog_card_tile_sx(post: dict, ctx: dict) -> str: """Single blog card tile as sx call (wire format) — pure data.""" slug = post.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") @@ -332,10 +331,10 @@ def _blog_card_tile_sx(post: dict, ctx: dict) -> str: if authors: kwargs["authors"] = authors - return sx_call("blog-card-tile", **kwargs) + return await render_to_sx("blog-card-tile", **kwargs) -def _at_bar_sx(post: dict, ctx: dict) -> str: +async def _at_bar_sx(post: dict, ctx: dict) -> str: """Tags + authors bar below a card as sx.""" tags = post.get("tags") or [] authors = post.get("authors") or [] @@ -355,11 +354,11 @@ def _at_bar_sx(post: dict, ctx: dict) -> str: for a in authors ] if authors else [] - return sx_call("blog-at-bar", tags=tag_data, authors=author_data) + return await render_to_sx("blog-at-bar", tags=tag_data, authors=author_data) -def _page_cards_sx(ctx: dict) -> str: +async def _page_cards_sx(ctx: dict) -> str: """Render page cards with sentinel (sx).""" pages = ctx.get("pages") or ctx.get("posts") or [] page_num = ctx.get("page", 1) @@ -370,23 +369,23 @@ def _page_cards_sx(ctx: dict) -> str: parts = [] for pg in pages: - parts.append(_page_card_sx(pg, ctx)) + parts.append(await _page_card_sx(pg, ctx)) if page_num < total_pages: current_local_href = ctx.get("current_local_href", "/index?type=pages") next_url = f"{current_local_href}&page={page_num + 1}" if "?" in current_local_href else f"{current_local_href}?page={page_num + 1}" - parts.append(sx_call("sentinel-simple", + parts.append(await render_to_sx("sentinel-simple", id=f"sentinel-{page_num}-d", next_url=next_url, )) elif pages: - parts.append(sx_call("end-of-results")) + parts.append(await render_to_sx("end-of-results")) else: - parts.append(sx_call("blog-no-pages")) + parts.append(await render_to_sx("blog-no-pages")) return "(<> " + " ".join(parts) + ")" if parts else "" -def _page_card_sx(page: dict, ctx: dict) -> str: +async def _page_card_sx(page: dict, ctx: dict) -> str: """Single page card as sx.""" slug = page.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") @@ -398,7 +397,7 @@ def _page_card_sx(page: dict, ctx: dict) -> str: fi = page.get("feature_image") excerpt = page.get("custom_excerpt") or page.get("excerpt", "") - return sx_call("blog-page-card", + return await render_to_sx("blog-page-card", href=href, hx_select=hx_select, title=page.get("title", ""), has_calendar=features.get("calendar", False), has_market=features.get("market", False), @@ -407,7 +406,7 @@ def _page_card_sx(page: dict, ctx: dict) -> str: ) -def _view_toggle_sx(ctx: dict) -> str: +async def _view_toggle_sx(ctx: dict) -> str: """View toggle bar (list/tile) for desktop.""" view = ctx.get("view") current_local_href = ctx.get("current_local_href", "/index") @@ -419,17 +418,17 @@ def _view_toggle_sx(ctx: dict) -> str: list_href = f"{current_local_href}" tile_href = f"{current_local_href}{'&' if '?' in current_local_href else '?'}view=tile" - list_svg_sx = sx_call("list-svg") - tile_svg_sx = sx_call("tile-svg") + list_svg_sx = await render_to_sx("list-svg") + tile_svg_sx = await render_to_sx("tile-svg") - return sx_call("view-toggle", + return await render_to_sx("view-toggle", list_href=list_href, tile_href=tile_href, hx_select=hx_select, list_cls=list_cls, tile_cls=tile_cls, storage_key="blog_view", list_svg=SxExpr(list_svg_sx), tile_svg=SxExpr(tile_svg_sx), ) -def _content_type_tabs_sx(ctx: dict) -> str: +async def _content_type_tabs_sx(ctx: dict) -> str: """Posts/Pages tabs.""" content_type = ctx.get("content_type", "posts") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -440,29 +439,29 @@ def _content_type_tabs_sx(ctx: dict) -> str: posts_cls = "bg-stone-700 text-white" if content_type != "pages" else "bg-stone-100 text-stone-600 hover:bg-stone-200" pages_cls = "bg-stone-700 text-white" if content_type == "pages" else "bg-stone-100 text-stone-600 hover:bg-stone-200" - return sx_call("blog-content-type-tabs", + return await render_to_sx("blog-content-type-tabs", posts_href=posts_href, pages_href=pages_href, hx_select=hx_select, posts_cls=posts_cls, pages_cls=pages_cls, ) -def _blog_main_panel_sx(ctx: dict) -> str: +async def _blog_main_panel_sx(ctx: dict) -> str: """Blog index main panel with tabs, toggle, and cards.""" content_type = ctx.get("content_type", "posts") view = ctx.get("view") - tabs = _content_type_tabs_sx(ctx) + tabs = await _content_type_tabs_sx(ctx) if content_type == "pages": - cards = _page_cards_sx(ctx) - return sx_call("blog-main-panel-pages", + cards = await _page_cards_sx(ctx) + return await render_to_sx("blog-main-panel-pages", tabs=SxExpr(tabs), cards=SxExpr(cards), ) else: - toggle = _view_toggle_sx(ctx) + toggle = await _view_toggle_sx(ctx) 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" - cards = _blog_cards_sx(ctx) - return sx_call("blog-main-panel-posts", + cards = await _blog_cards_sx(ctx) + return await render_to_sx("blog-main-panel-posts", tabs=SxExpr(tabs), toggle=SxExpr(toggle), grid_cls=grid_cls, cards=SxExpr(cards), ) @@ -472,48 +471,48 @@ def _blog_main_panel_sx(ctx: dict) -> str: # Desktop aside (filter sidebar) # --------------------------------------------------------------------------- -def _blog_aside_sx(ctx: dict) -> str: +async def _blog_aside_sx(ctx: dict) -> str: """Desktop aside with search, action buttons, and filters.""" - sd = search_desktop_sx(ctx) - ab = _action_buttons_sx(ctx) - tgf = _tag_groups_filter_sx(ctx) - af = _authors_filter_sx(ctx) - return sx_call("blog-aside", + sd = await search_desktop_sx(ctx) + ab = await _action_buttons_sx(ctx) + tgf = await _tag_groups_filter_sx(ctx) + af = await _authors_filter_sx(ctx) + return await render_to_sx("blog-aside", search=SxExpr(sd), action_buttons=SxExpr(ab), tag_groups_filter=SxExpr(tgf), authors_filter=SxExpr(af), ) -def _blog_filter_sx(ctx: dict) -> str: +async def _blog_filter_sx(ctx: dict) -> str: """Mobile filter (details/summary).""" # Mobile filter summary tags summary_parts = [] - tg_summary = _tag_groups_filter_summary_sx(ctx) - au_summary = _authors_filter_summary_sx(ctx) + tg_summary = await _tag_groups_filter_summary_sx(ctx) + au_summary = await _authors_filter_summary_sx(ctx) if tg_summary: summary_parts.append(tg_summary) if au_summary: summary_parts.append(au_summary) - search_sx = search_mobile_sx(ctx) + search_sx = await search_mobile_sx(ctx) if summary_parts: filter_content = "(<> " + search_sx + " " + " ".join(summary_parts) + ")" else: filter_content = search_sx - action_buttons = _action_buttons_sx(ctx) - tgf = _tag_groups_filter_sx(ctx) - af = _authors_filter_sx(ctx) + action_buttons = await _action_buttons_sx(ctx) + tgf = await _tag_groups_filter_sx(ctx) + af = await _authors_filter_sx(ctx) filter_details = "(<> " + tgf + " " + af + ")" - return sx_call("mobile-filter", + return await render_to_sx("mobile-filter", filter_summary=SxExpr(filter_content), action_buttons=SxExpr(action_buttons), filter_details=SxExpr(filter_details), ) -def _action_buttons_sx(ctx: dict) -> str: +async def _action_buttons_sx(ctx: dict) -> str: """New Post/Page + Drafts toggle buttons (sx).""" from quart import g @@ -529,13 +528,13 @@ def _action_buttons_sx(ctx: dict) -> str: if has_admin: new_href = call_url(ctx, "blog_url", "/new/") - parts.append(sx_call("blog-action-button", + parts.append(await render_to_sx("blog-action-button", href=new_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors", title="New Post", icon_class="fa fa-plus mr-1", label=" New Post", )) new_page_href = call_url(ctx, "blog_url", "/new-page/") - parts.append(sx_call("blog-action-button", + parts.append(await render_to_sx("blog-action-button", href=new_page_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors", title="New Page", icon_class="fa fa-plus mr-1", label=" New Page", @@ -544,26 +543,26 @@ def _action_buttons_sx(ctx: dict) -> str: if user and (draft_count or drafts): if drafts: off_href = f"{current_local_href}" - parts.append(sx_call("blog-drafts-button", + parts.append(await render_to_sx("blog-drafts-button", href=off_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors", title="Hide Drafts", label=" Drafts ", draft_count=str(draft_count), )) else: on_href = f"{current_local_href}{'&' if '?' in current_local_href else '?'}drafts=1" - parts.append(sx_call("blog-drafts-button-amber", + parts.append(await render_to_sx("blog-drafts-button-amber", href=on_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-amber-600 text-white text-sm hover:bg-amber-700 transition-colors", title="Show Drafts", label=" Drafts ", draft_count=str(draft_count), )) inner = "(<> " + " ".join(parts) + ")" if parts else "" - return sx_call("blog-action-buttons-wrapper", + return await render_to_sx("blog-action-buttons-wrapper", inner=SxExpr(inner) if inner else None, ) -def _tag_groups_filter_sx(ctx: dict) -> str: +async def _tag_groups_filter_sx(ctx: dict) -> str: """Tag group filter bar as sx.""" tag_groups = ctx.get("tag_groups") or [] selected_groups = ctx.get("selected_groups") or () @@ -573,7 +572,7 @@ def _tag_groups_filter_sx(ctx: dict) -> str: is_any = len(selected_groups) == 0 and len(selected_tags) == 0 any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" - li_parts = [sx_call("blog-filter-any-topic", cls=any_cls, hx_select=hx_select)] + li_parts = [await render_to_sx("blog-filter-any-topic", cls=any_cls, hx_select=hx_select)] for group in tag_groups: g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "") @@ -589,21 +588,21 @@ def _tag_groups_filter_sx(ctx: dict) -> str: cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" if g_fi: - icon = sx_call("blog-filter-group-icon-image", src=g_fi, name=g_name) + icon = await render_to_sx("blog-filter-group-icon-image", src=g_fi, name=g_name) else: style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;" - icon = sx_call("blog-filter-group-icon-color", style=style, initial=g_name[:1]) + icon = await render_to_sx("blog-filter-group-icon-color", style=style, initial=g_name[:1]) - li_parts.append(sx_call("blog-filter-group-li", + li_parts.append(await render_to_sx("blog-filter-group-li", cls=cls, hx_get=f"?group={g_slug}&page=1", hx_select=hx_select, icon=SxExpr(icon), name=g_name, count=str(g_count), )) items = "(<> " + " ".join(li_parts) + ")" - return sx_call("blog-filter-nav", items=SxExpr(items)) + return await render_to_sx("blog-filter-nav", items=SxExpr(items)) -def _authors_filter_sx(ctx: dict) -> str: +async def _authors_filter_sx(ctx: dict) -> str: """Author filter bar as sx.""" authors = ctx.get("authors") or [] selected_authors = ctx.get("selected_authors") or () @@ -612,7 +611,7 @@ def _authors_filter_sx(ctx: dict) -> str: is_any = len(selected_authors) == 0 any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" - li_parts = [sx_call("blog-filter-any-author", cls=any_cls, hx_select=hx_select)] + li_parts = [await render_to_sx("blog-filter-any-author", cls=any_cls, hx_select=hx_select)] for author in authors: a_slug = getattr(author, "slug", "") if hasattr(author, "slug") else author.get("slug", "") @@ -625,18 +624,18 @@ def _authors_filter_sx(ctx: dict) -> str: icon_sx = None if a_img: - icon_sx = sx_call("blog-filter-author-icon", src=a_img, name=a_name) + icon_sx = await render_to_sx("blog-filter-author-icon", src=a_img, name=a_name) - li_parts.append(sx_call("blog-filter-author-li", + li_parts.append(await render_to_sx("blog-filter-author-li", cls=cls, hx_get=f"?author={a_slug}&page=1", hx_select=hx_select, icon=SxExpr(icon_sx) if icon_sx else None, name=a_name, count=str(a_count), )) items = "(<> " + " ".join(li_parts) + ")" - return sx_call("blog-filter-nav", items=SxExpr(items)) + return await render_to_sx("blog-filter-nav", items=SxExpr(items)) -def _tag_groups_filter_summary_sx(ctx: dict) -> str: +async def _tag_groups_filter_summary_sx(ctx: dict) -> str: """Mobile filter summary for tag groups (sx).""" selected_groups = ctx.get("selected_groups") or () tag_groups = ctx.get("tag_groups") or [] @@ -650,11 +649,11 @@ def _tag_groups_filter_summary_sx(ctx: dict) -> str: names.append(g_name) if not names: return "" - return sx_call("blog-filter-summary", text=", ".join(names)) + return await render_to_sx("blog-filter-summary", text=", ".join(names)) -def _authors_filter_summary_sx(ctx: dict) -> str: +async def _authors_filter_summary_sx(ctx: dict) -> str: """Mobile filter summary for authors (sx).""" selected_authors = ctx.get("selected_authors") or () authors = ctx.get("authors") or [] @@ -668,7 +667,7 @@ def _authors_filter_summary_sx(ctx: dict) -> str: names.append(a_name) if not names: return "" - return sx_call("blog-filter-summary", text=", ".join(names)) + return await render_to_sx("blog-filter-summary", text=", ".join(names)) @@ -676,7 +675,7 @@ def _authors_filter_summary_sx(ctx: dict) -> str: # Post detail main panel # --------------------------------------------------------------------------- -def _post_main_panel_sx(ctx: dict) -> str: +async def _post_main_panel_sx(ctx: dict) -> str: """Post/page article content.""" from quart import g, url_for as qurl @@ -693,10 +692,10 @@ def _post_main_panel_sx(ctx: dict) -> str: edit_sx = "" if is_admin or (user and post.get("user_id") == getattr(user, "id", None)): edit_href = qurl("blog.post.admin.defpage_post_edit", slug=slug) - edit_sx = sx_call("blog-detail-edit-link", + edit_sx = await render_to_sx("blog-detail-edit-link", href=edit_href, hx_select=hx_select, ) - draft_sx = sx_call("blog-detail-draft", + draft_sx = await render_to_sx("blog-detail-draft", publish_requested=post.get("publish_requested"), edit=SxExpr(edit_sx) if edit_sx else None, ) @@ -708,7 +707,7 @@ def _post_main_panel_sx(ctx: dict) -> str: if user: liked = post.get("is_liked", False) like_url = call_url(ctx, "blog_url", f"/{slug}/like/toggle/") - like_sx = sx_call("blog-detail-like", + like_sx = await render_to_sx("blog-detail-like", like_url=like_url, hx_headers=f'{{"X-CSRFToken": "{_ctx_csrf(ctx)}"}}', heart="\u2764\ufe0f" if liked else "\U0001f90d", @@ -716,12 +715,12 @@ def _post_main_panel_sx(ctx: dict) -> str: excerpt_sx = "" if post.get("custom_excerpt"): - excerpt_sx = sx_call("blog-detail-excerpt", + excerpt_sx = await render_to_sx("blog-detail-excerpt", excerpt=post["custom_excerpt"], ) - at_bar = _at_bar_sx(post, ctx) - chrome_sx = sx_call("blog-detail-chrome", + at_bar = await _at_bar_sx(post, ctx) + chrome_sx = await render_to_sx("blog-detail-chrome", like=SxExpr(like_sx) if like_sx else None, excerpt=SxExpr(excerpt_sx) if excerpt_sx else None, at_bar=SxExpr(at_bar) if at_bar else None, @@ -731,7 +730,7 @@ def _post_main_panel_sx(ctx: dict) -> str: html_content = post.get("html", "") sx_content = post.get("sx_content", "") - return sx_call("blog-detail-main", + return await render_to_sx("blog-detail-main", draft=SxExpr(draft_sx) if draft_sx else None, chrome=SxExpr(chrome_sx) if chrome_sx else None, feature_image=fi, html_content=html_content, @@ -739,7 +738,7 @@ def _post_main_panel_sx(ctx: dict) -> str: ) -def _post_meta_sx(ctx: dict) -> str: +async def _post_meta_sx(ctx: dict) -> str: """Post SEO meta tags as sx (auto-hoisted to by sx.js).""" post = ctx.get("post") or {} base_title = ctx.get("base_title", "") @@ -771,7 +770,7 @@ def _post_meta_sx(ctx: dict) -> str: tw_title = post.get("twitter_title") or page_title is_article = not post.get("is_page") - return sx_call("blog-meta", + return await render_to_sx("blog-meta", robots=robots, page_title=page_title, desc=desc, canonical=canonical, og_type="article" if is_article else "website", og_title=og_title, image=image, @@ -784,12 +783,12 @@ def _post_meta_sx(ctx: dict) -> str: # Home page (Ghost "home" page) # --------------------------------------------------------------------------- -def _home_main_panel_sx(ctx: dict) -> str: +async def _home_main_panel_sx(ctx: dict) -> str: """Home page content — renders the Ghost page HTML or sx_content.""" post = ctx.get("post") or {} html = post.get("html", "") sx_content = post.get("sx_content", "") - return sx_call("blog-home-main", + return await render_to_sx("blog-home-main", html_content=html, sx_content=SxExpr(sx_content) if sx_content else None) @@ -810,24 +809,24 @@ def _settings_main_panel_sx(ctx: dict) -> str: return '(div :class "max-w-2xl mx-auto px-4 py-6")' -def _cache_main_panel_sx(ctx: dict) -> str: +async def _cache_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl csrf = _ctx_csrf(ctx) clear_url = qurl("settings.cache_clear") - return sx_call("blog-cache-panel", clear_url=clear_url, csrf=csrf) + return await render_to_sx("blog-cache-panel", clear_url=clear_url, csrf=csrf) # --------------------------------------------------------------------------- # Snippets main panel # --------------------------------------------------------------------------- -def _snippets_main_panel_sx(ctx: dict) -> str: - sl = _snippets_list_sx(ctx) - return sx_call("blog-snippets-panel", list=SxExpr(sl)) +async def _snippets_main_panel_sx(ctx: dict) -> str: + sl = await _snippets_list_sx(ctx) + return await render_to_sx("blog-snippets-panel", list=SxExpr(sl)) -def _snippets_list_sx(ctx: dict) -> str: +async def _snippets_list_sx(ctx: dict) -> str: """Snippets list with visibility badges and delete buttons.""" from quart import url_for as qurl, g @@ -838,7 +837,7 @@ def _snippets_list_sx(ctx: dict) -> str: user_id = getattr(user, "id", None) if not snippets: - return sx_call("empty-state", icon="fa fa-puzzle-piece", message="No snippets yet. Create one from the blog editor.") + return await render_to_sx("empty-state", icon="fa fa-puzzle-piece", message="No snippets yet. Create one from the blog editor.") badge_colours = { "private": "bg-stone-200 text-stone-700", @@ -861,10 +860,10 @@ def _snippets_list_sx(ctx: dict) -> str: patch_url = qurl("snippets.patch_visibility", snippet_id=s_id) opts = "" for v in ["private", "shared", "admin"]: - opts += sx_call("blog-snippet-option", + opts += await render_to_sx("blog-snippet-option", value=v, selected=(s_vis == v), label=v, ) - extra += sx_call("blog-snippet-visibility-select", + extra += await render_to_sx("blog-snippet-visibility-select", patch_url=patch_url, hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', options=SxExpr("(<> " + opts + ")") if opts else None, @@ -873,7 +872,7 @@ def _snippets_list_sx(ctx: dict) -> str: if s_uid == user_id or is_admin: del_url = qurl("snippets.delete_snippet", snippet_id=s_id) - extra += sx_call("delete-btn", + extra += await render_to_sx("delete-btn", url=del_url, trigger_target="#snippets-list", title="Delete snippet?", text=f'Delete \u201c{s_name}\u201d?', @@ -881,35 +880,35 @@ def _snippets_list_sx(ctx: dict) -> str: cls="px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0", ) - row_parts.append(sx_call("blog-snippet-row", + row_parts.append(await render_to_sx("blog-snippet-row", name=s_name, owner=owner, badge_cls=badge_cls, visibility=s_vis, extra=SxExpr("(<> " + extra + ")") if extra else None, )) rows = "(<> " + " ".join(row_parts) + ")" - return sx_call("blog-snippets-list", rows=SxExpr(rows)) + return await render_to_sx("blog-snippets-list", rows=SxExpr(rows)) # --------------------------------------------------------------------------- # Menu items main panel # --------------------------------------------------------------------------- -def _menu_items_main_panel_sx(ctx: dict) -> str: +async def _menu_items_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl new_url = qurl("menu_items.new_menu_item") - ml = _menu_items_list_sx(ctx) - return sx_call("blog-menu-items-panel", new_url=new_url, list=SxExpr(ml)) + ml = await _menu_items_list_sx(ctx) + return await render_to_sx("blog-menu-items-panel", new_url=new_url, list=SxExpr(ml)) -def _menu_items_list_sx(ctx: dict) -> str: +async def _menu_items_list_sx(ctx: dict) -> str: from quart import url_for as qurl menu_items = ctx.get("menu_items") or [] csrf = _ctx_csrf(ctx) if not menu_items: - return sx_call("empty-state", icon="fa fa-inbox", message="No menu items yet. Add one to get started!") + return await render_to_sx("empty-state", icon="fa fa-inbox", message="No menu items yet. Add one to get started!") row_parts = [] for item in menu_items: @@ -922,10 +921,10 @@ def _menu_items_list_sx(ctx: dict) -> str: edit_url = qurl("menu_items.edit_menu_item", item_id=i_id) del_url = qurl("menu_items.delete_menu_item_route", item_id=i_id) - img_sx = sx_call("img-or-placeholder", src=fi, alt=label, + img_sx = await render_to_sx("img-or-placeholder", src=fi, alt=label, size_cls="w-12 h-12 rounded-full object-cover flex-shrink-0") - row_parts.append(sx_call("blog-menu-item-row", + row_parts.append(await render_to_sx("blog-menu-item-row", img=SxExpr(img_sx), label=label, slug=slug, sort_order=str(sort), edit_url=edit_url, delete_url=del_url, confirm_text=f"Remove {label} from the menu?", @@ -933,14 +932,14 @@ def _menu_items_list_sx(ctx: dict) -> str: )) rows = "(<> " + " ".join(row_parts) + ")" - return sx_call("blog-menu-items-list", rows=SxExpr(rows)) + return await render_to_sx("blog-menu-items-list", rows=SxExpr(rows)) # --------------------------------------------------------------------------- # Tag groups main panel # --------------------------------------------------------------------------- -def _tag_groups_main_panel_sx(ctx: dict) -> str: +async def _tag_groups_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl groups = ctx.get("groups") or [] @@ -948,7 +947,7 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str: csrf = _ctx_csrf(ctx) create_url = qurl("blog.tag_groups_admin.create") - form_sx = sx_call("blog-tag-groups-create-form", + form_sx = await render_to_sx("blog-tag-groups-create-form", create_url=create_url, csrf=csrf, ) @@ -967,18 +966,18 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str: edit_href = qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id) if g_fi: - icon = sx_call("blog-tag-group-icon-image", src=g_fi, name=g_name) + icon = await render_to_sx("blog-tag-group-icon-image", src=g_fi, name=g_name) else: style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;" - icon = sx_call("blog-tag-group-icon-color", style=style, initial=g_name[:1]) + icon = await render_to_sx("blog-tag-group-icon-color", style=style, initial=g_name[:1]) - li_parts.append(sx_call("blog-tag-group-li", + li_parts.append(await render_to_sx("blog-tag-group-li", icon=SxExpr(icon), edit_href=edit_href, name=g_name, slug=g_slug, sort_order=str(g_sort), )) - groups_sx = sx_call("blog-tag-groups-list", items=SxExpr("(<> " + " ".join(li_parts) + ")")) + groups_sx = await render_to_sx("blog-tag-groups-list", items=SxExpr("(<> " + " ".join(li_parts) + ")")) else: - groups_sx = sx_call("empty-state", message="No tag groups yet.", cls="text-stone-500 text-sm") + groups_sx = await render_to_sx("empty-state", message="No tag groups yet.", cls="text-stone-500 text-sm") # Unassigned tags unassigned_sx = "" @@ -986,20 +985,20 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str: tag_spans = [] for tag in unassigned_tags: t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "") - tag_spans.append(sx_call("blog-unassigned-tag", name=t_name)) - unassigned_sx = sx_call("blog-unassigned-tags", + tag_spans.append(await render_to_sx("blog-unassigned-tag", name=t_name)) + unassigned_sx = await render_to_sx("blog-unassigned-tags", heading=f"Unassigned Tags ({len(unassigned_tags)})", spans=SxExpr("(<> " + " ".join(tag_spans) + ")"), ) - return sx_call("blog-tag-groups-main", + return await render_to_sx("blog-tag-groups-main", form=SxExpr(form_sx), groups=SxExpr(groups_sx), unassigned=SxExpr(unassigned_sx) if unassigned_sx else None, ) -def _tag_groups_edit_main_panel_sx(ctx: dict) -> str: +async def _tag_groups_edit_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl group = ctx.get("group") @@ -1023,24 +1022,24 @@ def _tag_groups_edit_main_panel_sx(ctx: dict) -> str: t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "") t_fi = getattr(tag, "feature_image", None) if hasattr(tag, "feature_image") else tag.get("feature_image") checked = t_id in assigned_tag_ids - img = sx_call("blog-tag-checkbox-image", src=t_fi) if t_fi else "" - tag_items.append(sx_call("blog-tag-checkbox", + img = (await render_to_sx("blog-tag-checkbox-image", src=t_fi)) if t_fi else "" + tag_items.append(await render_to_sx("blog-tag-checkbox", tag_id=str(t_id), checked=checked, img=SxExpr(img) if img else None, name=t_name, )) - edit_form = sx_call("blog-tag-group-edit-form", + edit_form = await render_to_sx("blog-tag-group-edit-form", save_url=save_url, csrf=csrf, name=g_name, colour=g_colour or "", sort_order=str(g_sort), feature_image=g_fi or "", tags=SxExpr("(<> " + " ".join(tag_items) + ")"), ) - del_form = sx_call("blog-tag-group-delete-form", + del_form = await render_to_sx("blog-tag-group-delete-form", delete_url=del_url, csrf=csrf, ) - return sx_call("blog-tag-group-edit-main", + return await render_to_sx("blog-tag-group-edit-main", edit_form=SxExpr(edit_form), delete_form=SxExpr(del_form), ) @@ -1061,68 +1060,64 @@ def _tag_groups_edit_main_panel_sx(ctx: dict) -> str: # ---- Home page ---- async def render_home_page(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) - post_hdr = _post_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + post_hdr = await _post_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + post_hdr + ")" - content = _home_main_panel_sx(ctx) - meta = _post_meta_sx(ctx) - menu = mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx)) - return full_page_sx(ctx, header_rows=header_rows, content=content, + content = await _home_main_panel_sx(ctx) + meta = await _post_meta_sx(ctx) + menu = mobile_menu_sx(await post_mobile_nav_sx(ctx), await mobile_root_nav_sx(ctx)) + return await full_page_sx(ctx, header_rows=header_rows, content=content, meta=meta, menu=menu) async def render_home_oob(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) - post_hdr = _post_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + post_hdr = await _post_header_sx(ctx) rows = "(<> " + root_hdr + " " + post_hdr + ")" - header_oob = _oob_header_sx("root-header-child", "post-header-child", rows) - content = _home_main_panel_sx(ctx) - return oob_page_sx(oobs=header_oob, content=content) + header_oob = await _oob_header_sx("root-header-child", "post-header-child", rows) + content = await _home_main_panel_sx(ctx) + return await oob_page_sx(oobs=header_oob, content=content) # ---- Blog index ---- async def render_blog_page(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) - blog_hdr = _blog_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + blog_hdr = await _blog_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + blog_hdr + ")" - content = _blog_main_panel_sx(ctx) - aside = _blog_aside_sx(ctx) - filter_sx = _blog_filter_sx(ctx) - return full_page_sx(ctx, header_rows=header_rows, content=content, + content = await _blog_main_panel_sx(ctx) + aside = await _blog_aside_sx(ctx) + filter_sx = await _blog_filter_sx(ctx) + return await full_page_sx(ctx, header_rows=header_rows, content=content, aside=aside, filter=filter_sx) async def render_blog_oob(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) - blog_hdr = _blog_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + blog_hdr = await _blog_header_sx(ctx) rows = "(<> " + root_hdr + " " + blog_hdr + ")" - header_oob = _oob_header_sx("root-header-child", "blog-header-child", rows) - content = _blog_main_panel_sx(ctx) - aside = _blog_aside_sx(ctx) - filter_sx = _blog_filter_sx(ctx) - return oob_page_sx(oobs=header_oob, content=content, aside=aside, + header_oob = await _oob_header_sx("root-header-child", "blog-header-child", rows) + content = await _blog_main_panel_sx(ctx) + aside = await _blog_aside_sx(ctx) + filter_sx = await _blog_filter_sx(ctx) + return await oob_page_sx(oobs=header_oob, content=content, aside=aside, filter=filter_sx) async def render_blog_cards(ctx: dict) -> str: """Pagination-only response (page > 1) — sx wire format.""" - return _blog_cards_sx(ctx) + return await _blog_cards_sx(ctx) async def render_blog_page_cards(ctx: dict) -> str: """Page cards pagination response.""" - return _page_cards_sx(ctx) + return await _page_cards_sx(ctx) # ---- New post/page editor panel ---- -def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> str: - """Build the WYSIWYG editor panel HTML (replaces _main_panel.html template). - - This is synchronous — it just assembles an HTML string from the current - request context (url_for, CSRF token, asset URLs, config). - """ +async def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> str: + """Build the WYSIWYG editor panel HTML (replaces _main_panel.html template).""" import os from quart import url_for as qurl, current_app from shared.browser.app.csrf import generate_csrf_token @@ -1148,18 +1143,18 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> # Error banner if save_error: - parts.append(sx_call("blog-editor-error", error=str(save_error))) + parts.append(await render_to_sx("blog-editor-error", error=str(save_error))) # Form structure - form_html = sx_call("blog-editor-form", + form_html = await render_to_sx("blog-editor-form", csrf=csrf, title_placeholder=title_placeholder, create_label=create_label, ) parts.append(form_html) # Editor CSS + inline styles + sx editor styles - parts.append(sx_call("blog-editor-styles", css_href=editor_css)) - parts.append(sx_call("sx-editor-styles")) + parts.append(await render_to_sx("blog-editor-styles", css_href=editor_css)) + parts.append(await render_to_sx("sx-editor-styles")) # Editor JS + init script init_js = ( @@ -1298,7 +1293,7 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> " }\n" "})();\n" ) - parts.append(sx_call("blog-editor-scripts", + parts.append(await render_to_sx("blog-editor-scripts", js_src=editor_js, sx_editor_js_src=sx_editor_js, init_js=init_js)) @@ -1309,35 +1304,35 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> # ---- New post/page ---- async def render_new_post_page(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) - blog_hdr = _blog_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + blog_hdr = await _blog_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + blog_hdr + ")" content = ctx.get("editor_html", "") - return full_page_sx(ctx, header_rows=header_rows, content=content) + return await full_page_sx(ctx, header_rows=header_rows, content=content) # ---- Post detail ---- async def render_post_page(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) - post_hdr = _post_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + post_hdr = await _post_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + post_hdr + ")" - content = _post_main_panel_sx(ctx) - meta = _post_meta_sx(ctx) - menu = mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx)) - return full_page_sx(ctx, header_rows=header_rows, content=content, + content = await _post_main_panel_sx(ctx) + meta = await _post_meta_sx(ctx) + menu = mobile_menu_sx(await post_mobile_nav_sx(ctx), await mobile_root_nav_sx(ctx)) + return await full_page_sx(ctx, header_rows=header_rows, content=content, meta=meta, menu=menu) async def render_post_oob(ctx: dict) -> str: - root_hdr = root_header_sx(ctx) # non-OOB (nested inside root-header-child) - post_hdr = _post_header_sx(ctx) + root_hdr = await root_header_sx(ctx) # non-OOB (nested inside root-header-child) + post_hdr = await _post_header_sx(ctx) rows = "(<> " + root_hdr + " " + post_hdr + ")" - post_oob = _oob_header_sx("root-header-child", "post-header-child", rows) - content = _post_main_panel_sx(ctx) - menu = mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx)) + post_oob = await _oob_header_sx("root-header-child", "post-header-child", rows) + content = await _post_main_panel_sx(ctx) + menu = mobile_menu_sx(await post_mobile_nav_sx(ctx), await mobile_root_nav_sx(ctx)) oobs = post_oob - return oob_page_sx(oobs=oobs, content=content, menu=menu) + return await oob_page_sx(oobs=oobs, content=content, menu=menu) # ---- Post admin ---- @@ -1462,14 +1457,14 @@ def _post_data_content_sx(ctx: dict) -> str: # =========================================================================== -def _preview_main_panel_sx(ctx: dict) -> str: +async def _preview_main_panel_sx(ctx: dict) -> str: """Build the preview panel with 4 expandable sections.""" sections: list[str] = [] # 1. Prettified SX source sx_pretty = ctx.get("sx_pretty", "") if sx_pretty: - sections.append(sx_call("blog-preview-section", + sections.append(await render_to_sx("blog-preview-section", title="S-Expression Source", content=SxExpr(sx_pretty), )) @@ -1477,7 +1472,7 @@ def _preview_main_panel_sx(ctx: dict) -> str: # 2. Prettified Lexical JSON json_pretty = ctx.get("json_pretty", "") if json_pretty: - sections.append(sx_call("blog-preview-section", + sections.append(await render_to_sx("blog-preview-section", title="Lexical JSON", content=SxExpr(json_pretty), )) @@ -1486,7 +1481,7 @@ def _preview_main_panel_sx(ctx: dict) -> str: sx_rendered = ctx.get("sx_rendered", "") if sx_rendered: rendered_sx = f'(div :class "blog-content prose max-w-none" (raw! {sx_serialize(sx_rendered)}))' - sections.append(sx_call("blog-preview-section", + sections.append(await render_to_sx("blog-preview-section", title="SX Rendered", content=SxExpr(rendered_sx), )) @@ -1495,7 +1490,7 @@ def _preview_main_panel_sx(ctx: dict) -> str: lex_rendered = ctx.get("lex_rendered", "") if lex_rendered: rendered_sx = f'(div :class "blog-content prose max-w-none" (raw! {sx_serialize(lex_rendered)}))' - sections.append(sx_call("blog-preview-section", + sections.append(await render_to_sx("blog-preview-section", title="Lexical Rendered", content=SxExpr(rendered_sx), )) @@ -1504,12 +1499,12 @@ def _preview_main_panel_sx(ctx: dict) -> str: return '(div :class "p-8 text-stone-500" "No content to preview.")' inner = " ".join(sections) - return sx_call("blog-preview-panel", sections=SxExpr(f"(<> {inner})")) + return await render_to_sx("blog-preview-panel", sections=SxExpr(f"(<> {inner})")) # =========================================================================== -def _post_entries_content_sx(ctx: dict) -> str: +async def _post_entries_content_sx(ctx: dict) -> str: """Build post entries panel natively (replaces _types/post_entries/_main_panel.html).""" from quart import g, url_for as qurl from shared.utils import host_url @@ -1519,7 +1514,7 @@ def _post_entries_content_sx(ctx: dict) -> str: post_slug = g.post_data["post"]["slug"] # Associated entries list (reuse existing render function) - assoc_html = render_associated_entries(all_calendars, associated_entry_ids, post_slug) + assoc_html = await render_associated_entries(all_calendars, associated_entry_ids, post_slug) # Calendar browser cal_items: list[str] = [] @@ -1677,12 +1672,10 @@ def _raw_html_sx(html: str) -> str: return "(raw! " + sx_serialize(html) + ")" -def _post_edit_content_sx(ctx: dict) -> str: +async def _post_edit_content_sx(ctx: dict) -> str: """Build WYSIWYG editor panel as SX expression (edit page).""" from quart import url_for as qurl, current_app, g, request as qrequest from shared.browser.app.csrf import generate_csrf_token - from shared.sx.helpers import sx_call - from shared.sx.parser import SxExpr ghost_post = ctx.get("ghost_post", {}) or {} save_success = ctx.get("save_success", False) @@ -1752,10 +1745,10 @@ def _post_edit_content_sx(ctx: dict) -> str: # Error banner if save_error: - parts.append(sx_call("blog-editor-error", error=save_error)) + parts.append(await render_to_sx("blog-editor-error", error=save_error)) # Form (sx_content_val populates #sx-content-input; JS reads from there) - parts.append(sx_call("blog-editor-edit-form", + parts.append(await render_to_sx("blog-editor-edit-form", csrf=csrf, updated_at=str(updated_at), title_val=title_val, @@ -1773,11 +1766,11 @@ def _post_edit_content_sx(ctx: dict) -> str: )) # Publish-mode JS - parts.append(sx_call("blog-editor-publish-js", already_emailed=already_emailed)) + parts.append(await render_to_sx("blog-editor-publish-js", already_emailed=already_emailed)) # Editor CSS + styles - parts.append(sx_call("blog-editor-styles", css_href=editor_css)) - parts.append(sx_call("sx-editor-styles")) + parts.append(await render_to_sx("blog-editor-styles", css_href=editor_css)) + parts.append(await render_to_sx("sx-editor-styles")) # Editor JS + init init_js = ( @@ -1878,7 +1871,7 @@ def _post_edit_content_sx(ctx: dict) -> str: ' }, 50); }' '})();' ) - parts.append(sx_call("blog-editor-scripts", + parts.append(await render_to_sx("blog-editor-scripts", js_src=editor_js, sx_editor_js_src=sx_editor_js, init_js=init_js)) @@ -2039,15 +2032,15 @@ def _post_settings_content_sx(ctx: dict) -> str: # ---- Like toggle button (delegates to market impl) ---- -def render_like_toggle_button(slug: str, liked: bool, like_url: str) -> str: +async def render_like_toggle_button(slug: str, liked: bool, like_url: str) -> str: """Render a like toggle button for HTMX POST response.""" from market.sx.sx_components import render_like_toggle_button as _market_like - return _market_like(slug, liked, like_url=like_url, item_type="post") + return await _market_like(slug, liked, like_url=like_url, item_type="post") # ---- Snippets list ---- -def render_snippets_list(snippets, is_admin: bool) -> str: +async def render_snippets_list(snippets, is_admin: bool) -> str: """Render the snippets list fragment for HTMX DELETE/PATCH responses.""" from shared.browser.app.csrf import generate_csrf_token from quart import g @@ -2057,12 +2050,12 @@ def render_snippets_list(snippets, is_admin: bool) -> str: "is_admin": is_admin, "csrf_token": generate_csrf_token(), } - return _snippets_list_sx(ctx) + return await _snippets_list_sx(ctx) # ---- Menu items list + nav OOB ---- -def render_menu_items_list(menu_items) -> str: +async def render_menu_items_list(menu_items) -> str: """Render the menu items list fragment for HTMX responses.""" from shared.browser.app.csrf import generate_csrf_token @@ -2070,7 +2063,7 @@ def render_menu_items_list(menu_items) -> str: "menu_items": menu_items, "csrf_token": generate_csrf_token(), } - return _menu_items_list_sx(ctx) + return await _menu_items_list_sx(ctx) def render_menu_item_form(menu_item=None) -> str: @@ -2161,19 +2154,19 @@ document.addEventListener('click', function(e) {{ return html -def render_page_search_results(pages, query, page, has_more) -> str: +async def render_page_search_results(pages, query, page, has_more) -> str: """Render page search results (replaces _types/menu_items/_page_search_results.html).""" from quart import url_for as qurl if not pages and query: - return sx_call("page-search-empty", query=query) + return await render_to_sx("page-search-empty", query=query) if not pages: return "" items = [] for post in pages: - items.append(sx_call("page-search-item", + items.append(await render_to_sx("page-search-item", id=post.id, title=post.title, slug=post.slug, feature_image=post.feature_image or None)) @@ -2181,17 +2174,17 @@ def render_page_search_results(pages, query, page, has_more) -> str: sentinel = "" if has_more: search_url = qurl("menu_items.search_pages_route") - sentinel = sx_call("page-search-sentinel", + sentinel = await render_to_sx("page-search-sentinel", url=search_url, query=query, next_page=page + 1) items_sx = "(<> " + " ".join(items) + ")" - return sx_call("page-search-results", + return await render_to_sx("page-search-results", items=SxExpr(items_sx), sentinel=SxExpr(sentinel) if sentinel else None) -def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: +async def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: """Render the OOB nav update for menu items. Produces the same DOM structure as ``_types/menu_items/_nav_oob.html``: @@ -2201,7 +2194,7 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: from quart import request as qrequest if not menu_items: - return sx_call("blog-nav-empty", wrapper_id="menu-items-nav-wrapper") + return await render_to_sx("blog-nav-empty", wrapper_id="menu-items-nav-wrapper") # Resolve URL helpers from context or fall back to template globals if ctx is None: @@ -2249,23 +2242,23 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: selected = "true" if (item_slug == first_seg or item_slug == app_name) else "false" - img_sx = sx_call("img-or-placeholder", src=fi, alt=label, + img_sx = await render_to_sx("img-or-placeholder", src=fi, alt=label, size_cls="w-8 h-8 rounded-full object-cover flex-shrink-0") if item_slug != "cart": - item_parts.append(sx_call("blog-nav-item-link", + item_parts.append(await render_to_sx("blog-nav-item-link", href=href, hx_get=f"/{item_slug}/", selected=selected, nav_cls=nav_button_cls, img=SxExpr(img_sx), label=label, )) else: - item_parts.append(sx_call("blog-nav-item-plain", + item_parts.append(await render_to_sx("blog-nav-item-plain", href=href, selected=selected, nav_cls=nav_button_cls, img=SxExpr(img_sx), label=label, )) items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "" - return sx_call("scroll-nav-wrapper", + return await render_to_sx("scroll-nav-wrapper", wrapper_id="menu-items-nav-wrapper", container_id=container_id, arrow_cls=arrow_cls, left_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft - 200", @@ -2277,7 +2270,7 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: # ---- Features panel ---- -def render_features_panel(features: dict, post: dict, +async def render_features_panel(features: dict, post: dict, sumup_configured: bool, sumup_merchant_code: str, sumup_checkout_prefix: str) -> str: @@ -2291,7 +2284,7 @@ def render_features_panel(features: dict, post: dict, hs_trigger = "on change trigger submit on closest
" - form_sx = sx_call("blog-features-form", + form_sx = await render_to_sx("blog-features-form", features_url=features_url, calendar_checked=bool(features.get("calendar")), market_checked=bool(features.get("market")), @@ -2302,14 +2295,14 @@ def render_features_panel(features: dict, post: dict, if features.get("calendar") or features.get("market"): placeholder = "\u2022" * 8 if sumup_configured else "sup_sk_..." - sumup_sx = sx_call("blog-sumup-form", + sumup_sx = await render_to_sx("blog-sumup-form", sumup_url=sumup_url, merchant_code=sumup_merchant_code, placeholder=placeholder, sumup_configured=sumup_configured, checkout_prefix=sumup_checkout_prefix, ) - return sx_call("blog-features-panel", + return await render_to_sx("blog-features-panel", form=SxExpr(form_sx), sumup=SxExpr(sumup_sx) if sumup_sx else None, ) @@ -2317,7 +2310,7 @@ def render_features_panel(features: dict, post: dict, # ---- Markets panel ---- -def render_markets_panel(markets, post: dict) -> str: +async def render_markets_panel(markets, post: dict) -> str: """Render the markets panel fragment for HTMX responses.""" from shared.utils import host_url from quart import url_for as qurl @@ -2332,22 +2325,22 @@ def render_markets_panel(markets, post: dict) -> str: m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "") m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "") del_url = host_url(qurl("blog.post.admin.delete_market", slug=slug, market_slug=m_slug)) - li_parts.append(sx_call("blog-market-item", + li_parts.append(await render_to_sx("blog-market-item", name=m_name, slug=m_slug, delete_url=del_url, confirm_text=f"Delete market '{m_name}'?", )) - list_sx = sx_call("blog-markets-list", items=SxExpr("(<> " + " ".join(li_parts) + ")")) + list_sx = await render_to_sx("blog-markets-list", items=SxExpr("(<> " + " ".join(li_parts) + ")")) else: - list_sx = sx_call("blog-markets-empty") + list_sx = await render_to_sx("blog-markets-empty") - return sx_call("blog-markets-panel", + return await render_to_sx("blog-markets-panel", list=SxExpr(list_sx), create_url=create_url, ) # ---- Associated entries ---- -def render_associated_entries(all_calendars, associated_entry_ids, post_slug: str) -> str: +async def render_associated_entries(all_calendars, associated_entry_ids, post_slug: str) -> str: """Render the associated entries panel for HTMX POST responses.""" from shared.browser.app.csrf import generate_csrf_token from quart import url_for as qurl @@ -2377,13 +2370,13 @@ def render_associated_entries(all_calendars, associated_entry_ids, post_slug: st toggle_url = host_url(qurl("blog.post.admin.toggle_entry", slug=post_slug, entry_id=e_id)) - img_sx = sx_call("blog-entry-image", src=cal_fi, title=cal_title) + img_sx = await render_to_sx("blog-entry-image", src=cal_fi, title=cal_title) date_str = e_start.strftime("%A, %B %d, %Y at %H:%M") if e_start else "" if e_end: date_str += f" \u2013 {e_end.strftime('%H:%M')}" - entry_items.append(sx_call("blog-associated-entry", + entry_items.append(await render_to_sx("blog-associated-entry", confirm_text=f"This will remove {e_name} from this post", toggle_url=toggle_url, hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', @@ -2392,18 +2385,18 @@ def render_associated_entries(all_calendars, associated_entry_ids, post_slug: st )) if has_entries: - content_sx = sx_call("blog-associated-entries-content", + content_sx = await render_to_sx("blog-associated-entries-content", items=SxExpr("(<> " + " ".join(entry_items) + ")"), ) else: - content_sx = sx_call("blog-associated-entries-empty") + content_sx = await render_to_sx("blog-associated-entries-empty") - return sx_call("blog-associated-entries-panel", content=SxExpr(content_sx)) + return await render_to_sx("blog-associated-entries-panel", content=SxExpr(content_sx)) # ---- Nav entries OOB ---- -def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict | None = None) -> str: +async def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict | None = None) -> str: """Render the OOB nav entries swap. Produces the ``entries-calendars-nav-wrapper`` OOB element with links @@ -2419,7 +2412,7 @@ def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict has_items = bool(entries_list or calendars) if not has_items: - return sx_call("blog-nav-entries-empty") + return await render_to_sx("blog-nav-entries-empty") events_url_fn = ctx.get("events_url") @@ -2467,7 +2460,7 @@ def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict href = events_url_fn(entry_path) if events_url_fn else entry_path - item_parts.append(sx_call("calendar-entry-nav", + item_parts.append(await render_to_sx("calendar-entry-nav", href=href, nav_class=nav_cls, name=e_name, date_str=date_str, )) @@ -2478,13 +2471,13 @@ def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict cal_path = f"/{post_slug}/{cal_slug}/" href = events_url_fn(cal_path) if events_url_fn else cal_path - item_parts.append(sx_call("blog-nav-calendar-item", + item_parts.append(await render_to_sx("blog-nav-calendar-item", href=href, nav_cls=nav_cls, name=cal_name, )) items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "" - return sx_call("scroll-nav-wrapper", + return await render_to_sx("scroll-nav-wrapper", wrapper_id="entries-calendars-nav-wrapper", container_id="associated-items-container", arrow_cls="entries-nav-arrow", left_hs="on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft - 200", diff --git a/blog/sxc/pages/__init__.py b/blog/sxc/pages/__init__.py index 0536ba1..dbecb2d 100644 --- a/blog/sxc/pages/__init__.py +++ b/blog/sxc/pages/__init__.py @@ -129,148 +129,148 @@ def _register_blog_layouts() -> None: # --- Blog layout (root + blog header) --- -def _blog_full(ctx: dict, **kw: Any) -> str: +async def _blog_full(ctx: dict, **kw: Any) -> str: from shared.sx.helpers import root_header_sx from sx.sx_components import _blog_header_sx - root_hdr = root_header_sx(ctx) - blog_hdr = _blog_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + blog_hdr = await _blog_header_sx(ctx) return "(<> " + root_hdr + " " + blog_hdr + ")" -def _blog_oob(ctx: dict, **kw: Any) -> str: +async def _blog_oob(ctx: dict, **kw: Any) -> str: from shared.sx.helpers import root_header_sx, oob_header_sx from sx.sx_components import _blog_header_sx - root_hdr = root_header_sx(ctx) - blog_hdr = _blog_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + blog_hdr = await _blog_header_sx(ctx) rows = "(<> " + root_hdr + " " + blog_hdr + ")" - return oob_header_sx("root-header-child", "blog-header-child", rows) + return await oob_header_sx("root-header-child", "blog-header-child", rows) # --- Settings layout (root + settings header) --- -def _settings_full(ctx: dict, **kw: Any) -> str: +async def _settings_full(ctx: dict, **kw: Any) -> str: from shared.sx.helpers import root_header_sx from sx.sx_components import _settings_header_sx - root_hdr = root_header_sx(ctx) - settings_hdr = _settings_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + settings_hdr = await _settings_header_sx(ctx) return "(<> " + root_hdr + " " + settings_hdr + ")" -def _settings_oob(ctx: dict, **kw: Any) -> str: +async def _settings_oob(ctx: dict, **kw: Any) -> str: from shared.sx.helpers import root_header_sx, oob_header_sx from sx.sx_components import _settings_header_sx - root_hdr = root_header_sx(ctx) - settings_hdr = _settings_header_sx(ctx) + root_hdr = await root_header_sx(ctx) + settings_hdr = await _settings_header_sx(ctx) rows = "(<> " + root_hdr + " " + settings_hdr + ")" - return oob_header_sx("root-header-child", "root-settings-header-child", rows) + return await oob_header_sx("root-header-child", "root-settings-header-child", rows) -def _settings_mobile(ctx: dict, **kw: Any) -> str: +async def _settings_mobile(ctx: dict, **kw: Any) -> str: from sx.sx_components import _settings_nav_sx - return _settings_nav_sx(ctx) + return await _settings_nav_sx(ctx) # --- Sub-settings helpers --- -def _sub_settings_full(ctx: dict, row_id: str, child_id: str, +async def _sub_settings_full(ctx: dict, row_id: str, child_id: str, endpoint: str, icon: str, label: str) -> str: from shared.sx.helpers import root_header_sx from sx.sx_components import _settings_header_sx, _sub_settings_header_sx from quart import url_for as qurl - root_hdr = root_header_sx(ctx) - settings_hdr = _settings_header_sx(ctx) - sub_hdr = _sub_settings_header_sx(row_id, child_id, + root_hdr = await root_header_sx(ctx) + settings_hdr = await _settings_header_sx(ctx) + sub_hdr = await _sub_settings_header_sx(row_id, child_id, qurl(endpoint), icon, label, ctx) return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")" -def _sub_settings_oob(ctx: dict, row_id: str, child_id: str, +async def _sub_settings_oob(ctx: dict, row_id: str, child_id: str, endpoint: str, icon: str, label: str) -> str: from shared.sx.helpers import oob_header_sx from sx.sx_components import _settings_header_sx, _sub_settings_header_sx from quart import url_for as qurl - settings_hdr_oob = _settings_header_sx(ctx, oob=True) - sub_hdr = _sub_settings_header_sx(row_id, child_id, + settings_hdr_oob = await _settings_header_sx(ctx, oob=True) + sub_hdr = await _sub_settings_header_sx(row_id, child_id, qurl(endpoint), icon, label, ctx) - sub_oob = oob_header_sx("root-settings-header-child", child_id, sub_hdr) + sub_oob = await oob_header_sx("root-settings-header-child", child_id, sub_hdr) return "(<> " + settings_hdr_oob + " " + sub_oob + ")" # --- Cache --- -def _cache_full(ctx: dict, **kw: Any) -> str: - return _sub_settings_full(ctx, "cache-row", "cache-header-child", +async def _cache_full(ctx: dict, **kw: Any) -> str: + return await _sub_settings_full(ctx, "cache-row", "cache-header-child", "defpage_cache_page", "refresh", "Cache") -def _cache_oob(ctx: dict, **kw: Any) -> str: - return _sub_settings_oob(ctx, "cache-row", "cache-header-child", +async def _cache_oob(ctx: dict, **kw: Any) -> str: + return await _sub_settings_oob(ctx, "cache-row", "cache-header-child", "defpage_cache_page", "refresh", "Cache") # --- Snippets --- -def _snippets_full(ctx: dict, **kw: Any) -> str: - return _sub_settings_full(ctx, "snippets-row", "snippets-header-child", +async def _snippets_full(ctx: dict, **kw: Any) -> str: + return await _sub_settings_full(ctx, "snippets-row", "snippets-header-child", "defpage_snippets_page", "puzzle-piece", "Snippets") -def _snippets_oob(ctx: dict, **kw: Any) -> str: - return _sub_settings_oob(ctx, "snippets-row", "snippets-header-child", +async def _snippets_oob(ctx: dict, **kw: Any) -> str: + return await _sub_settings_oob(ctx, "snippets-row", "snippets-header-child", "defpage_snippets_page", "puzzle-piece", "Snippets") # --- Menu Items --- -def _menu_items_full(ctx: dict, **kw: Any) -> str: - return _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child", +async def _menu_items_full(ctx: dict, **kw: Any) -> str: + return await _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child", "defpage_menu_items_page", "bars", "Menu Items") -def _menu_items_oob(ctx: dict, **kw: Any) -> str: - return _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child", +async def _menu_items_oob(ctx: dict, **kw: Any) -> str: + return await _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child", "defpage_menu_items_page", "bars", "Menu Items") # --- Tag Groups --- -def _tag_groups_full(ctx: dict, **kw: Any) -> str: - return _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child", +async def _tag_groups_full(ctx: dict, **kw: Any) -> str: + return await _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child", "defpage_tag_groups_page", "tags", "Tag Groups") -def _tag_groups_oob(ctx: dict, **kw: Any) -> str: - return _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child", +async def _tag_groups_oob(ctx: dict, **kw: Any) -> str: + return await _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child", "defpage_tag_groups_page", "tags", "Tag Groups") # --- Tag Group Edit --- -def _tag_group_edit_full(ctx: dict, **kw: Any) -> str: +async def _tag_group_edit_full(ctx: dict, **kw: Any) -> str: from quart import request g_id = (request.view_args or {}).get("id") from quart import url_for as qurl from shared.sx.helpers import root_header_sx from sx.sx_components import _settings_header_sx, _sub_settings_header_sx - root_hdr = root_header_sx(ctx) - settings_hdr = _settings_header_sx(ctx) - sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child", + root_hdr = await root_header_sx(ctx) + settings_hdr = await _settings_header_sx(ctx) + sub_hdr = await _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child", qurl("defpage_tag_group_edit", id=g_id), "tags", "Tag Groups", ctx) return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")" -def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str: +async def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str: from quart import request g_id = (request.view_args or {}).get("id") from quart import url_for as qurl from shared.sx.helpers import oob_header_sx from sx.sx_components import _settings_header_sx, _sub_settings_header_sx - settings_hdr_oob = _settings_header_sx(ctx, oob=True) - sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child", + settings_hdr_oob = await _settings_header_sx(ctx, oob=True) + sub_hdr = await _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child", qurl("defpage_tag_group_edit", id=g_id), "tags", "Tag Groups", ctx) - sub_oob = oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr) + sub_oob = await oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr) return "(<> " + settings_hdr_oob + " " + sub_oob + ")" @@ -302,12 +302,12 @@ def _register_blog_helpers() -> None: async def _h_editor_content(**kw): from sx.sx_components import render_editor_panel - return render_editor_panel() + return await render_editor_panel() async def _h_editor_page_content(**kw): from sx.sx_components import render_editor_panel - return render_editor_panel(is_page=True) + return await render_editor_panel(is_page=True) # --- Post admin helpers --- @@ -391,7 +391,7 @@ async def _h_post_preview_content(slug=None, **kw): from sx.sx_components import _preview_main_panel_sx tctx = await get_template_context() tctx.update(preview_ctx) - return _preview_main_panel_sx(tctx) + return await _preview_main_panel_sx(tctx) async def _h_post_entries_content(slug=None, **kw): @@ -415,7 +415,7 @@ async def _h_post_entries_content(slug=None, **kw): tctx = await get_template_context() tctx["all_calendars"] = all_calendars tctx["associated_entry_ids"] = associated_entry_ids - return _post_entries_content_sx(tctx) + return await _post_entries_content_sx(tctx) async def _h_post_settings_content(slug=None, **kw): @@ -468,7 +468,7 @@ async def _h_post_edit_content(slug=None, **kw): tctx["save_success"] = save_success tctx["save_error"] = save_error tctx["newsletters"] = newsletters - return _post_edit_content_sx(tctx) + return await _post_edit_content_sx(tctx) # --- Settings helpers --- @@ -484,7 +484,7 @@ async def _h_cache_content(**kw): from shared.sx.page import get_template_context from sx.sx_components import _cache_main_panel_sx tctx = await get_template_context() - return _cache_main_panel_sx(tctx) + return await _cache_main_panel_sx(tctx) # --- Snippets helper --- @@ -506,7 +506,7 @@ async def _h_snippets_content(**kw): tctx = await get_template_context() tctx["snippets"] = rows tctx["is_admin"] = is_admin - return _snippets_main_panel_sx(tctx) + return await _snippets_main_panel_sx(tctx) # --- Menu Items helper --- @@ -519,7 +519,7 @@ async def _h_menu_items_content(**kw): from sx.sx_components import _menu_items_main_panel_sx tctx = await get_template_context() tctx["menu_items"] = menu_items - return _menu_items_main_panel_sx(tctx) + return await _menu_items_main_panel_sx(tctx) # --- Tag Groups helpers --- @@ -539,7 +539,7 @@ async def _h_tag_groups_content(**kw): from sx.sx_components import _tag_groups_main_panel_sx tctx = await get_template_context() tctx.update({"groups": groups, "unassigned_tags": unassigned}) - return _tag_groups_main_panel_sx(tctx) + return await _tag_groups_main_panel_sx(tctx) async def _h_tag_group_edit_content(id=None, **kw): @@ -571,4 +571,4 @@ async def _h_tag_group_edit_content(id=None, **kw): "all_tags": all_tags, "assigned_tag_ids": set(assigned_rows), }) - return _tag_groups_edit_main_panel_sx(tctx) + return await _tag_groups_edit_main_panel_sx(tctx) diff --git a/cart/bp/page_admin/routes.py b/cart/bp/page_admin/routes.py index f77492c..566365d 100644 --- a/cart/bp/page_admin/routes.py +++ b/cart/bp/page_admin/routes.py @@ -49,7 +49,7 @@ def register(): from shared.sx.page import get_template_context from sx.sx_components import render_cart_payments_panel ctx = await get_template_context() - html = render_cart_payments_panel(ctx) + html = await render_cart_payments_panel(ctx) return sx_response(html) return bp diff --git a/cart/sx/sx_components.py b/cart/sx/sx_components.py index f8174a2..2e05620 100644 --- a/cart/sx/sx_components.py +++ b/cart/sx/sx_components.py @@ -16,9 +16,9 @@ from shared.sx.helpers import ( post_header_sx as _shared_post_header_sx, search_desktop_sx, search_mobile_sx, full_page_sx, oob_page_sx, header_child_sx, - sx_call, SxExpr, + render_to_sx, ) -from shared.sx.parser import serialize +from shared.sx.parser import SxExpr from shared.infrastructure.urls import cart_url # Load cart-specific .sx components + handlers at import time @@ -69,12 +69,12 @@ async def _post_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> st """Build post-level header row from page_post DTO, using shared helper.""" ctx = _ensure_post_ctx(ctx, page_post) ctx = await _ensure_container_nav(ctx) - return _shared_post_header_sx(ctx, oob=oob) + return await _shared_post_header_sx(ctx, oob=oob) -def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str: +async def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the cart section header row.""" - return sx_call( + return await render_to_sx( "menu-row-sx", id="cart-row", level=1, colour="sky", link_href=call_url(ctx, "cart_url", "/"), @@ -83,17 +83,17 @@ def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str: ) -def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str: +async def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str: """Build the per-page cart header row.""" slug = page_post.slug if page_post else "" title = ((page_post.title if page_post else None) or "")[:160] label_parts = [] if page_post and page_post.feature_image: - label_parts.append(sx_call("cart-page-label-img", src=page_post.feature_image)) + label_parts.append(await render_to_sx("cart-page-label-img", src=page_post.feature_image)) label_parts.append(f'(span "{escape(title)}")') label_sx = "(<> " + " ".join(label_parts) + ")" - nav_sx = sx_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/")) - return sx_call( + nav_sx = await render_to_sx("cart-all-carts-link", href=call_url(ctx, "cart_url", "/")) + return await render_to_sx( "menu-row-sx", id="page-cart-row", level=2, colour="sky", link_href=call_url(ctx, "cart_url", f"/{slug}/"), @@ -102,26 +102,26 @@ def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str ) -def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str: +async def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the account section header row (for orders).""" - return sx_call( + return await render_to_sx( "auth-header-row-simple", account_url=call_url(ctx, "account_url", ""), oob=oob, ) -def _orders_header_sx(ctx: dict, list_url: str) -> str: +async def _orders_header_sx(ctx: dict, list_url: str) -> str: """Build the orders section header row.""" - return sx_call("orders-header-row", list_url=list_url) + return await render_to_sx("orders-header-row", list_url=list_url) -def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False, +async def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False, selected: str = "") -> str: """Build the page-level admin header row.""" slug = page_post.slug if page_post else "" ctx = _ensure_post_ctx(ctx, page_post) - return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) + return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected) # --------------------------------------------------------------------------- @@ -190,24 +190,25 @@ async def render_orders_page(ctx: dict, orders: list, page: int, detail_url_prefix = pfx + url_for_fn("orders.order.order_detail", order_id=0).rsplit("0/", 1)[0] order_dicts = [_serialize_order(o) for o in orders] - content = sx_call("orders-list-content", - orders=SxExpr(serialize(order_dicts)), + content = await render_to_sx("orders-list-content", + orders=order_dicts, page=page, total_pages=total_pages, rows_url=rows_url, detail_url_prefix=detail_url_prefix) - hdr = root_header_sx(ctx) - auth = _auth_header_sx(ctx) - orders_hdr = _orders_header_sx(ctx, list_url) - auth_child = sx_call( + hdr = await root_header_sx(ctx) + auth = await _auth_header_sx(ctx) + orders_hdr = await _orders_header_sx(ctx, list_url) + auth_child_inner = await render_to_sx("header-child-sx", id="auth-header-child", inner=SxExpr(orders_hdr)) + auth_child = await render_to_sx( "header-child-sx", - inner=SxExpr("(<> " + auth + " " + sx_call("header-child-sx", id="auth-header-child", inner=SxExpr(orders_hdr)) + ")"), + inner=SxExpr("(<> " + auth + " " + auth_child_inner + ")"), ) header_rows = "(<> " + hdr + " " + auth_child + ")" - filt = sx_call("order-list-header", search_mobile=SxExpr(search_mobile_sx(ctx))) - return full_page_sx(ctx, header_rows=header_rows, + filt = await render_to_sx("order-list-header", search_mobile=SxExpr(await search_mobile_sx(ctx))) + return await full_page_sx(ctx, header_rows=header_rows, filter=filt, - aside=search_desktop_sx(ctx), + aside=await search_desktop_sx(ctx), content=content) @@ -222,20 +223,21 @@ async def render_orders_rows(ctx: dict, orders: list, page: int, detail_url_prefix = pfx + url_for_fn("orders.order.order_detail", order_id=0).rsplit("0/", 1)[0] order_dicts = [_serialize_order(o) for o in orders] - parts = [sx_call("order-row-pair", - order=SxExpr(serialize(od)), - detail_url_prefix=detail_url_prefix) - for od in order_dicts] + parts = [] + for od in order_dicts: + parts.append(await render_to_sx("order-row-pair", + order=od, + detail_url_prefix=detail_url_prefix)) if page < total_pages: next_url = list_url + qs_fn(page=page + 1) - parts.append(sx_call( + parts.append(await render_to_sx( "infinite-scroll", url=next_url, page=page, total_pages=total_pages, id_prefix="orders", colspan=5, )) else: - parts.append(sx_call("order-end-row")) + parts.append(await render_to_sx("order-end-row")) return "(<> " + " ".join(parts) + ")" @@ -255,24 +257,25 @@ async def render_orders_oob(ctx: dict, orders: list, page: int, detail_url_prefix = pfx + url_for_fn("orders.order.order_detail", order_id=0).rsplit("0/", 1)[0] order_dicts = [_serialize_order(o) for o in orders] - content = sx_call("orders-list-content", - orders=SxExpr(serialize(order_dicts)), + content = await render_to_sx("orders-list-content", + orders=order_dicts, page=page, total_pages=total_pages, rows_url=rows_url, detail_url_prefix=detail_url_prefix) - auth_oob = _auth_header_sx(ctx, oob=True) - auth_child_oob = sx_call( + auth_oob = await _auth_header_sx(ctx, oob=True) + orders_hdr = await _orders_header_sx(ctx, list_url) + auth_child_oob = await render_to_sx( "oob-header-sx", parent_id="auth-header-child", - row=SxExpr(_orders_header_sx(ctx, list_url)), + row=SxExpr(orders_hdr), ) - root_oob = root_header_sx(ctx, oob=True) + root_oob = await root_header_sx(ctx, oob=True) oobs = "(<> " + auth_oob + " " + auth_child_oob + " " + root_oob + ")" - filt = sx_call("order-list-header", search_mobile=SxExpr(search_mobile_sx(ctx))) - return oob_page_sx(oobs=oobs, + filt = await render_to_sx("order-list-header", search_mobile=SxExpr(await search_mobile_sx(ctx))) + return await oob_page_sx(oobs=oobs, filter=filt, - aside=search_desktop_sx(ctx), + aside=await search_desktop_sx(ctx), content=content) @@ -296,29 +299,32 @@ async def render_order_page(ctx: dict, order: Any, order_data = _serialize_order(order) cal_data = [_serialize_calendar_entry(e) for e in (calendar_entries or [])] - main = sx_call("order-detail-content", - order=SxExpr(serialize(order_data)), - calendar_entries=SxExpr(serialize(cal_data))) - filt = sx_call("order-detail-filter-content", - order=SxExpr(serialize(order_data)), + main = await render_to_sx("order-detail-content", + order=order_data, + calendar_entries=cal_data) + filt = await render_to_sx("order-detail-filter-content", + order=order_data, list_url=list_url, recheck_url=recheck_url, pay_url=pay_url, csrf=generate_csrf_token()) - hdr = root_header_sx(ctx) - order_row = sx_call( + hdr = await root_header_sx(ctx) + order_row = await render_to_sx( "menu-row-sx", id="order-row", level=3, colour="sky", link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp", ) - order_child = sx_call( + auth = await _auth_header_sx(ctx) + orders_hdr = await _orders_header_sx(ctx, list_url) + orders_child = await render_to_sx("header-child-sx", id="orders-header-child", inner=SxExpr(order_row)) + auth_inner = "(<> " + orders_hdr + " " + orders_child + ")" + auth_child = await render_to_sx("header-child-sx", id="auth-header-child", inner=SxExpr(auth_inner)) + order_child = await render_to_sx( "header-child-sx", - inner=SxExpr("(<> " + _auth_header_sx(ctx) + " " + sx_call("header-child-sx", id="auth-header-child", inner=SxExpr( - "(<> " + _orders_header_sx(ctx, list_url) + " " + sx_call("header-child-sx", id="orders-header-child", inner=SxExpr(order_row)) + ")" - )) + ")"), + inner=SxExpr("(<> " + auth + " " + auth_child + ")"), ) header_rows = "(<> " + hdr + " " + order_child + ")" - return full_page_sx(ctx, header_rows=header_rows, filter=filt, content=main) + return await full_page_sx(ctx, header_rows=header_rows, filter=filt, content=main) async def render_order_oob(ctx: dict, order: Any, @@ -337,27 +343,27 @@ async def render_order_oob(ctx: dict, order: Any, order_data = _serialize_order(order) cal_data = [_serialize_calendar_entry(e) for e in (calendar_entries or [])] - main = sx_call("order-detail-content", - order=SxExpr(serialize(order_data)), - calendar_entries=SxExpr(serialize(cal_data))) - filt = sx_call("order-detail-filter-content", - order=SxExpr(serialize(order_data)), + main = await render_to_sx("order-detail-content", + order=order_data, + calendar_entries=cal_data) + filt = await render_to_sx("order-detail-filter-content", + order=order_data, list_url=list_url, recheck_url=recheck_url, pay_url=pay_url, csrf=generate_csrf_token()) - order_row_oob = sx_call( + order_row_oob = await render_to_sx( "menu-row-sx", id="order-row", level=3, colour="sky", link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp", oob=True, ) - orders_child_oob = sx_call("oob-header-sx", + orders_child_oob = await render_to_sx("oob-header-sx", parent_id="orders-header-child", row=SxExpr(order_row_oob)) - root_oob = root_header_sx(ctx, oob=True) + root_oob = await root_header_sx(ctx, oob=True) oobs = "(<> " + orders_child_oob + " " + root_oob + ")" - return oob_page_sx(oobs=oobs, filter=filt, content=main) + return await oob_page_sx(oobs=oobs, filter=filt, content=main) # --------------------------------------------------------------------------- @@ -370,25 +376,25 @@ async def render_checkout_error_page(ctx: dict, error: str | None = None, err_msg = error or "Unexpected error while creating the hosted checkout session." order_sx = None if order: - order_sx = sx_call("checkout-error-order-id", oid=f"#{order.id}") + order_sx = await render_to_sx("checkout-error-order-id", oid=f"#{order.id}") back_url = cart_url("/") - hdr = root_header_sx(ctx) - filt = sx_call("checkout-error-header") - content = sx_call( + hdr = await root_header_sx(ctx) + filt = await render_to_sx("checkout-error-header") + content = await render_to_sx( "checkout-error-content", msg=err_msg, order=SxExpr(order_sx) if order_sx else None, back_url=back_url, ) - return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content) + return await full_page_sx(ctx, header_rows=hdr, filter=filt, content=content) # --------------------------------------------------------------------------- # Public API: POST response renderers # --------------------------------------------------------------------------- -def render_cart_payments_panel(ctx: dict) -> str: +async def render_cart_payments_panel(ctx: dict) -> str: """Render the payments config panel for PUT response.""" page_config = ctx.get("page_config") pc_data = None @@ -398,5 +404,5 @@ def render_cart_payments_panel(ctx: dict) -> str: "sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "", "sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "", } - return sx_call("cart-payments-content", - page_config=SxExpr(serialize(pc_data)) if pc_data else None) + return await render_to_sx("cart-payments-content", + page_config=pc_data) diff --git a/cart/sxc/pages/__init__.py b/cart/sxc/pages/__init__.py index b41a861..dd2576a 100644 --- a/cart/sxc/pages/__init__.py +++ b/cart/sxc/pages/__init__.py @@ -27,31 +27,35 @@ def _register_cart_layouts() -> None: register_custom_layout("cart-admin", _cart_admin_full, _cart_admin_oob) -def _cart_page_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, sx_call, SxExpr +async def _cart_page_full(ctx: dict, **kw: Any) -> str: + from shared.sx.helpers import root_header_sx, render_to_sx + from shared.sx.parser import SxExpr from sx.sx_components import _cart_header_sx, _page_cart_header_sx page_post = ctx.get("page_post") - root_hdr = root_header_sx(ctx) - child = _cart_header_sx(ctx) - page_hdr = _page_cart_header_sx(ctx, page_post) - nested = sx_call( + root_hdr = await root_header_sx(ctx) + child = await _cart_header_sx(ctx) + page_hdr = await _page_cart_header_sx(ctx, page_post) + inner_child = await render_to_sx("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr)) + nested = await render_to_sx( "header-child-sx", - inner=SxExpr("(<> " + child + " " + sx_call("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr)) + ")"), + inner=SxExpr("(<> " + child + " " + inner_child + ")"), ) return "(<> " + root_hdr + " " + nested + ")" -def _cart_page_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, sx_call, SxExpr +async def _cart_page_oob(ctx: dict, **kw: Any) -> str: + from shared.sx.helpers import root_header_sx, render_to_sx + from shared.sx.parser import SxExpr from sx.sx_components import _cart_header_sx, _page_cart_header_sx page_post = ctx.get("page_post") - child_oob = sx_call("oob-header-sx", + page_hdr = await _page_cart_header_sx(ctx, page_post) + child_oob = await render_to_sx("oob-header-sx", parent_id="cart-header-child", - row=SxExpr(_page_cart_header_sx(ctx, page_post))) - cart_hdr_oob = _cart_header_sx(ctx, oob=True) - root_hdr_oob = root_header_sx(ctx, oob=True) + row=SxExpr(page_hdr)) + cart_hdr_oob = await _cart_header_sx(ctx, oob=True) + root_hdr_oob = await root_header_sx(ctx, oob=True) return "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")" @@ -61,9 +65,9 @@ async def _cart_admin_full(ctx: dict, **kw: Any) -> str: page_post = ctx.get("page_post") selected = kw.get("selected", "") - root_hdr = root_header_sx(ctx) + root_hdr = await root_header_sx(ctx) post_hdr = await _post_header_sx(ctx, page_post) - admin_hdr = _cart_page_admin_header_sx(ctx, page_post, selected=selected) + admin_hdr = await _cart_page_admin_header_sx(ctx, page_post, selected=selected) return "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")" @@ -72,7 +76,7 @@ async def _cart_admin_oob(ctx: dict, **kw: Any) -> str: page_post = ctx.get("page_post") selected = kw.get("selected", "") - return _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected) + return await _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected) # --------------------------------------------------------------------------- @@ -244,22 +248,21 @@ def _build_summary_data(ctx: dict, cart: list, cal_entries: list, tickets: list, async def _h_overview_content(**kw): from quart import g - from shared.sx.helpers import sx_call, SxExpr - from shared.sx.parser import serialize + from shared.sx.helpers import render_to_sx from shared.infrastructure.urls import cart_url from bp.cart.services import get_cart_grouped_by_page page_groups = await get_cart_grouped_by_page(g.s) grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d] - return sx_call("cart-overview-content", - page_groups=SxExpr(serialize(grp_dicts)), + return await render_to_sx("cart-overview-content", + page_groups=grp_dicts, cart_url_base=cart_url("")) async def _h_page_cart_content(page_slug=None, **kw): from quart import g - from shared.sx.helpers import sx_call, SxExpr - from shared.sx.parser import serialize + from shared.sx.helpers import render_to_sx + from shared.sx.parser import SxExpr from shared.sx.page import get_template_context from bp.cart.services import total, calendar_total, ticket_total from bp.cart.services.page_cart import ( @@ -277,7 +280,7 @@ async def _h_page_cart_content(page_slug=None, **kw): sd = _build_summary_data(ctx, cart, cal_entries, page_tickets, total, calendar_total, ticket_total) - summary_sx = sx_call("cart-summary-from-data", + summary_sx = await render_to_sx("cart-summary-from-data", item_count=sd["item_count"], grand_total=sd["grand_total"], symbol=sd["symbol"], @@ -286,10 +289,10 @@ async def _h_page_cart_content(page_slug=None, **kw): login_href=sd.get("login_href"), user_email=sd.get("user_email")) - return sx_call("cart-page-cart-content", - cart_items=SxExpr(serialize([_serialize_cart_item(i) for i in cart])), - cal_entries=SxExpr(serialize([_serialize_cal_entry(e) for e in cal_entries])), - ticket_groups=SxExpr(serialize([_serialize_ticket_group(tg) for tg in ticket_groups])), + return await render_to_sx("cart-page-cart-content", + cart_items=[_serialize_cart_item(i) for i in cart], + cal_entries=[_serialize_cal_entry(e) for e in cal_entries], + ticket_groups=[_serialize_ticket_group(tg) for tg in ticket_groups], summary=SxExpr(summary_sx)) @@ -299,8 +302,7 @@ async def _h_cart_admin_content(page_slug=None, **kw): async def _h_cart_payments_content(page_slug=None, **kw): from shared.sx.page import get_template_context - from shared.sx.helpers import sx_call, SxExpr - from shared.sx.parser import serialize + from shared.sx.helpers import render_to_sx ctx = await get_template_context() page_config = ctx.get("page_config") @@ -311,5 +313,5 @@ async def _h_cart_payments_content(page_slug=None, **kw): "sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "", "sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "", } - return sx_call("cart-payments-content", - page_config=SxExpr(serialize(pc_data)) if pc_data else None) + return await render_to_sx("cart-payments-content", + page_config=pc_data) diff --git a/events/bp/all_events/routes.py b/events/bp/all_events/routes.py index b657b32..6534ea9 100644 --- a/events/bp/all_events/routes.py +++ b/events/bp/all_events/routes.py @@ -126,7 +126,7 @@ def register() -> Blueprint: frag_params["session_id"] = ident["session_id"] from sx.sx_components import render_ticket_widget - widget_html = render_ticket_widget(entry, qty, "/all-tickets/adjust") + 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 e7fcc01..5f9abdb 100644 --- a/events/bp/calendar/admin/routes.py +++ b/events/bp/calendar/admin/routes.py @@ -19,7 +19,7 @@ def register(): @require_admin async def calendar_description_edit(calendar_slug: str, **kwargs): from sx.sx_components import render_calendar_description_edit - html = render_calendar_description_edit(g.calendar) + html = await render_calendar_description_edit(g.calendar) return sx_response(html) @@ -35,7 +35,7 @@ def register(): await g.s.flush() from sx.sx_components import render_calendar_description - html = render_calendar_description(g.calendar, oob=True) + html = await render_calendar_description(g.calendar, oob=True) return sx_response(html) @@ -43,7 +43,7 @@ def register(): @require_admin async def calendar_description_view(calendar_slug: str, **kwargs): from sx.sx_components import render_calendar_description - html = render_calendar_description(g.calendar) + html = await render_calendar_description(g.calendar) return sx_response(html) return bp diff --git a/events/bp/calendar/routes.py b/events/bp/calendar/routes.py index aa07ad9..d6e470a 100644 --- a/events/bp/calendar/routes.py +++ b/events/bp/calendar/routes.py @@ -201,7 +201,7 @@ def register(): from shared.sx.page import get_template_context from sx.sx_components import _calendar_admin_main_panel_html ctx = await get_template_context() - html = _calendar_admin_main_panel_html(ctx) + html = await _calendar_admin_main_panel_html(ctx) return sx_response(html) @@ -220,7 +220,7 @@ def register(): from shared.sx.page import get_template_context from sx.sx_components import render_calendars_list_panel ctx = await get_template_context() - html = render_calendars_list_panel(ctx) + html = await render_calendars_list_panel(ctx) if post_data: from shared.services.entry_associations import get_associated_entries @@ -236,7 +236,7 @@ def register(): ).scalars().all() associated_entries = await get_associated_entries(post_id) - nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) + nav_oob = await render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) html = html + nav_oob return sx_response(html) diff --git a/events/bp/calendar_entries/routes.py b/events/bp/calendar_entries/routes.py index 2ba164f..08b8475 100644 --- a/events/bp/calendar_entries/routes.py +++ b/events/bp/calendar_entries/routes.py @@ -259,7 +259,7 @@ def register(): } from sx.sx_components import render_day_main_panel - html = render_day_main_panel(ctx) + 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 "")) @@ -280,12 +280,12 @@ def register(): day_slots = list(result.scalars()) from sx.sx_components import render_entry_add_form - return sx_response(render_entry_add_form(g.calendar, day, month, year, day_slots)) + 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 sx.sx_components import render_entry_add_button - return sx_response(render_entry_add_button(g.calendar, day, month, year)) + 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 4139966..0df4002 100644 --- a/events/bp/calendar_entry/routes.py +++ b/events/bp/calendar_entry/routes.py @@ -112,7 +112,7 @@ def register(): # Render OOB nav from sx.sx_components import render_day_entries_nav_oob - return render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date) + return await render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date) async def get_post_nav_oob(entry_id: int): """Helper to generate OOB update for post entries nav when entry state changes""" @@ -149,7 +149,7 @@ def register(): # Render OOB nav for this post from sx.sx_components import render_post_nav_entries_oob - nav_oob = render_post_nav_entries_oob(associated_entries, calendars, post) + nav_oob = await render_post_nav_entries_oob(associated_entries, calendars, post) nav_oobs.append(nav_oob) return "".join(nav_oobs) @@ -257,7 +257,7 @@ def register(): day_slots = list(result.scalars()) from sx.sx_components import render_entry_edit_form - return sx_response(render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots)) + return sx_response(await render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots)) @bp.put("/") @require_admin @@ -423,7 +423,7 @@ def register(): from sx.sx_components import _entry_main_panel_html tctx = await get_template_context() - html = _entry_main_panel_html(tctx) + html = await _entry_main_panel_html(tctx) return sx_response(html + nav_oob) @@ -449,7 +449,7 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) from sx.sx_components import render_entry_optioned - html = render_entry_optioned(g.entry, g.calendar, day, month, year) + html = await render_entry_optioned(g.entry, g.calendar, day, month, year) return sx_response(html + day_nav_oob + post_nav_oob) @bp.post("/decline/") @@ -474,7 +474,7 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) from sx.sx_components import render_entry_optioned - html = render_entry_optioned(g.entry, g.calendar, day, month, year) + html = await render_entry_optioned(g.entry, g.calendar, day, month, year) return sx_response(html + day_nav_oob + post_nav_oob) @bp.post("/provisional/") @@ -499,7 +499,7 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) from sx.sx_components import render_entry_optioned - html = render_entry_optioned(g.entry, g.calendar, day, month, year) + html = await render_entry_optioned(g.entry, g.calendar, day, month, year) return sx_response(html + day_nav_oob + post_nav_oob) @bp.post("/tickets/") @@ -543,7 +543,7 @@ def register(): # Return just the tickets fragment (targeted by hx-target="#entry-tickets-...") await g.s.refresh(g.entry) from sx.sx_components import render_entry_tickets_config - html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year")) + 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) @bp.get("/posts/search/") @@ -559,7 +559,7 @@ def register(): va = request.view_args or {} from sx.sx_components import render_post_search_results - return sx_response(render_post_search_results( + return sx_response(await render_post_search_results( search_posts, query, page, total_pages, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), @@ -594,8 +594,8 @@ def register(): # Return updated posts list + OOB nav update from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob va = request.view_args or {} - html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) - nav_oob = render_entry_posts_nav_oob(entry_posts) + 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) return sx_response(html + nav_oob) @bp.delete("/posts//") @@ -616,8 +616,8 @@ def register(): # Return updated posts list + OOB nav update from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob va = request.view_args or {} - html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) - nav_oob = render_entry_posts_nav_oob(entry_posts) + 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) return sx_response(html + nav_oob) return bp diff --git a/events/bp/calendars/routes.py b/events/bp/calendars/routes.py index 350a939..a62d262 100644 --- a/events/bp/calendars/routes.py +++ b/events/bp/calendars/routes.py @@ -69,7 +69,7 @@ def register(): from shared.sx.page import get_template_context from sx.sx_components import render_calendars_list_panel ctx = await get_template_context() - html = render_calendars_list_panel(ctx) + html = await render_calendars_list_panel(ctx) # Blog-embedded mode: also update post nav if post_data: @@ -85,7 +85,7 @@ def register(): ).scalars().all() associated_entries = await get_associated_entries(post_id) - nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) + nav_oob = await render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) html = html + nav_oob return sx_response(html) diff --git a/events/bp/markets/routes.py b/events/bp/markets/routes.py index 2335a86..1457163 100644 --- a/events/bp/markets/routes.py +++ b/events/bp/markets/routes.py @@ -44,7 +44,7 @@ def register(): from shared.sx.page import get_template_context from sx.sx_components import render_markets_list_panel ctx = await get_template_context() - return sx_response(render_markets_list_panel(ctx)) + return sx_response(await render_markets_list_panel(ctx)) @bp.delete("//") @require_admin @@ -57,6 +57,6 @@ def register(): from shared.sx.page import get_template_context from sx.sx_components import render_markets_list_panel ctx = await get_template_context() - return sx_response(render_markets_list_panel(ctx)) + return sx_response(await render_markets_list_panel(ctx)) return bp diff --git a/events/bp/page/routes.py b/events/bp/page/routes.py index bb1a700..87962a1 100644 --- a/events/bp/page/routes.py +++ b/events/bp/page/routes.py @@ -107,7 +107,7 @@ def register() -> Blueprint: frag_params["session_id"] = ident["session_id"] from sx.sx_components import render_ticket_widget - widget_html = render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust") + 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 496e892..28a7449 100644 --- a/events/bp/slot/routes.py +++ b/events/bp/slot/routes.py @@ -36,7 +36,7 @@ def register(): if not slot: return await make_response("Not found", 404) from sx.sx_components import render_slot_edit_form - return sx_response(render_slot_edit_form(slot, g.calendar)) + return sx_response(await render_slot_edit_form(slot, g.calendar)) @bp.get("/view/") @require_admin @@ -45,7 +45,7 @@ def register(): if not slot: return await make_response("Not found", 404) from sx.sx_components import render_slot_main_panel - return sx_response(render_slot_main_panel(slot, g.calendar)) + return sx_response(await render_slot_main_panel(slot, g.calendar)) @bp.delete("/") @require_admin @@ -54,7 +54,7 @@ def register(): await svc_delete_slot(g.s, slot_id) slots = await svc_list_slots(g.s, g.calendar.id) from sx.sx_components import render_slots_table - return sx_response(render_slots_table(slots, g.calendar)) + return sx_response(await render_slots_table(slots, g.calendar)) @bp.put("/") @require_admin @@ -136,7 +136,7 @@ def register(): ), 422 from sx.sx_components import render_slot_main_panel - return sx_response(render_slot_main_panel(slot, g.calendar, oob=True)) + 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 e8443d0..37d9e70 100644 --- a/events/bp/slots/routes.py +++ b/events/bp/slots/routes.py @@ -111,19 +111,19 @@ def register(): # Success → re-render the slots table slots = await svc_list_slots(g.s, g.calendar.id) from sx.sx_components import render_slots_table - return sx_response(render_slots_table(slots, g.calendar)) + return sx_response(await render_slots_table(slots, g.calendar)) @bp.get("/add") @require_admin async def add_form(**kwargs): from sx.sx_components import render_slot_add_form - return sx_response(render_slot_add_form(g.calendar)) + return sx_response(await render_slot_add_form(g.calendar)) @bp.get("/add-button") @require_admin async def add_button(**kwargs): from sx.sx_components import render_slot_add_button - return sx_response(render_slot_add_button(g.calendar)) + 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 993c969..1eddfcc 100644 --- a/events/bp/ticket_admin/routes.py +++ b/events/bp/ticket_admin/routes.py @@ -54,7 +54,7 @@ def register() -> Blueprint: tickets = await get_tickets_for_entry(g.s, entry_id) from sx.sx_components import render_entry_tickets_admin - html = render_entry_tickets_admin(entry, tickets) + html = await render_entry_tickets_admin(entry, tickets) return sx_response(html) @bp.get("/lookup/") @@ -71,9 +71,9 @@ def register() -> Blueprint: ticket = await get_ticket_by_code(g.s, code) from sx.sx_components import render_lookup_result if not ticket: - return sx_response(render_lookup_result(None, "Ticket not found")) + return sx_response(await render_lookup_result(None, "Ticket not found")) - return sx_response(render_lookup_result(ticket, None)) + return sx_response(await render_lookup_result(ticket, None)) @bp.post("//checkin/") @require_admin @@ -84,9 +84,9 @@ def register() -> Blueprint: from sx.sx_components import render_checkin_result if not success: - return sx_response(render_checkin_result(False, error, None)) + return sx_response(await render_checkin_result(False, error, None)) ticket = await get_ticket_by_code(g.s, code) - return sx_response(render_checkin_result(True, None, ticket)) + return sx_response(await render_checkin_result(True, None, ticket)) return bp diff --git a/events/bp/ticket_type/routes.py b/events/bp/ticket_type/routes.py index 09e1191..66d26bc 100644 --- a/events/bp/ticket_type/routes.py +++ b/events/bp/ticket_type/routes.py @@ -32,7 +32,7 @@ def register(): from sx.sx_components import render_ticket_type_edit_form va = request.view_args or {} - return sx_response(render_ticket_type_edit_form( + return sx_response(await render_ticket_type_edit_form( ticket_type, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) @@ -47,7 +47,7 @@ def register(): from sx.sx_components import render_ticket_type_main_panel va = request.view_args or {} - return sx_response(render_ticket_type_main_panel( + return sx_response(await render_ticket_type_main_panel( ticket_type, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) @@ -114,7 +114,7 @@ def register(): # Return updated view with OOB flag from sx.sx_components import render_ticket_type_main_panel va = request.view_args or {} - return sx_response(render_ticket_type_main_panel( + return sx_response(await render_ticket_type_main_panel( ticket_type, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), oob=True, @@ -133,7 +133,7 @@ def register(): ticket_types = await svc_list_ticket_types(g.s, g.entry.id) from sx.sx_components import render_ticket_types_table va = request.view_args or {} - return sx_response(render_ticket_types_table( + return sx_response(await render_ticket_types_table( ticket_types, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) diff --git a/events/bp/ticket_types/routes.py b/events/bp/ticket_types/routes.py index 2163c34..b65cb76 100644 --- a/events/bp/ticket_types/routes.py +++ b/events/bp/ticket_types/routes.py @@ -95,7 +95,7 @@ def register(): ticket_types = await svc_list_ticket_types(g.s, g.entry.id) from sx.sx_components import render_ticket_types_table va = request.view_args or {} - return sx_response(render_ticket_types_table( + return sx_response(await render_ticket_types_table( ticket_types, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) @@ -106,7 +106,7 @@ def register(): """Show the add ticket type form.""" from sx.sx_components import render_ticket_type_add_form va = request.view_args or {} - return sx_response(render_ticket_type_add_form( + return sx_response(await render_ticket_type_add_form( g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) @@ -117,7 +117,7 @@ def register(): """Show the add ticket type button.""" from sx.sx_components import render_ticket_type_add_button va = request.view_args or {} - return sx_response(render_ticket_type_add_button( + return sx_response(await render_ticket_type_add_button( g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) diff --git a/events/bp/tickets/routes.py b/events/bp/tickets/routes.py index 54e8586..7d4106f 100644 --- a/events/bp/tickets/routes.py +++ b/events/bp/tickets/routes.py @@ -127,7 +127,7 @@ def register() -> Blueprint: cart_count = summary.count + summary.calendar_count + summary.ticket_count from sx.sx_components import render_buy_result - return sx_response(render_buy_result(entry, created, remaining, cart_count)) + return sx_response(await render_buy_result(entry, created, remaining, cart_count)) @bp.post("/adjust/") @clear_cache(tag="calendars", tag_scope="all") @@ -250,7 +250,7 @@ def register() -> Blueprint: cart_count = summary.count + summary.calendar_count + summary.ticket_count from sx.sx_components import render_adjust_response - return sx_response(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/sx/sx_components.py b/events/sx/sx_components.py index 8326e25..338e6db 100644 --- a/events/sx/sx_components.py +++ b/events/sx/sx_components.py @@ -13,7 +13,7 @@ from markupsafe import escape from shared.sx.jinja_bridge import load_service_components from shared.sx.helpers import ( - call_url, get_asset_url, sx_call, + call_url, get_asset_url, render_to_sx, root_header_sx, post_header_sx, post_admin_header_sx, oob_header_sx, header_child_sx, full_page_sx, oob_page_sx, @@ -85,12 +85,12 @@ async def _ensure_container_nav(ctx: dict) -> dict: return {**ctx, "container_nav": events_nav + market_nav} -def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: +async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the post-level header row — delegates to shared sx helper.""" - return post_header_sx(ctx, oob=oob) + return await post_header_sx(ctx, oob=oob) -def _post_nav_sx(ctx: dict) -> str: +async def _post_nav_sx(ctx: dict) -> str: """Post desktop nav: calendar links + container nav (markets, etc.).""" from quart import url_for, g @@ -104,7 +104,7 @@ def _post_nav_sx(ctx: dict) -> str: cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "") href = url_for("calendar.get", calendar_slug=cal_slug) is_sel = (cal_slug == current_cal_slug) - parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar", + 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.) @@ -143,13 +143,13 @@ def _post_nav_sx(ctx: dict) -> str: # Calendars header # --------------------------------------------------------------------------- -def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str: +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 sx_call("menu-row-sx", id="calendars-row", level=3, + return await render_to_sx("menu-row-sx", id="calendars-row", level=3, link_href=link_href, - link_label_content=SxExpr(sx_call("events-calendars-label")), + link_label_content=SxExpr(await render_to_sx("events-calendars-label")), child_id="calendars-header-child", oob=oob) @@ -157,7 +157,7 @@ def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str: # Calendar header # --------------------------------------------------------------------------- -def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str: +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") @@ -168,18 +168,18 @@ def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str: cal_desc = getattr(calendar, "description", "") or "" link_href = url_for("calendar.get", calendar_slug=cal_slug) - label_html = sx_call("events-calendar-label", + label_html = await render_to_sx("events-calendar-label", name=cal_name, description=cal_desc) # Desktop nav: slots + admin - nav_html = _calendar_nav_sx(ctx) + nav_html = await _calendar_nav_sx(ctx) - return sx_call("menu-row-sx", id="calendar-row", level=3, + 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) -def _calendar_nav_sx(ctx: dict) -> str: +async def _calendar_nav_sx(ctx: dict) -> str: """Calendar desktop nav: Slots + admin link.""" from quart import url_for calendar = ctx.get("calendar") @@ -192,11 +192,11 @@ def _calendar_nav_sx(ctx: dict) -> str: parts = [] slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug) - parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock", + 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(sx_call("nav-link", href=admin_href, icon="fa fa-cog", + parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog", select_colours=select_colours)) return "".join(parts) @@ -205,7 +205,7 @@ def _calendar_nav_sx(ctx: dict) -> str: # Day header # --------------------------------------------------------------------------- -def _day_header_sx(ctx: dict, *, oob: bool = False) -> str: +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") @@ -223,17 +223,17 @@ def _day_header_sx(ctx: dict, *, oob: bool = False) -> str: month=day_date.month, day=day_date.day, ) - label_html = sx_call("events-day-label", + label_html = await render_to_sx("events-day-label", date_str=day_date.strftime("%A %d %B %Y")) - nav_html = _day_nav_sx(ctx) + nav_html = await _day_nav_sx(ctx) - return sx_call("menu-row-sx", id="day-row", level=4, + 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) -def _day_nav_sx(ctx: dict) -> str: +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") @@ -260,11 +260,11 @@ def _day_nav_sx(ctx: dict) -> str: ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - entry_links.append(sx_call("events-day-entry-link", + 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(sx_call("events-day-entries-nav", inner=SxExpr(inner))) + parts.append(await render_to_sx("events-day-entries-nav", inner=SxExpr(inner))) if is_admin and day_date: admin_href = url_for( @@ -274,7 +274,7 @@ def _day_nav_sx(ctx: dict) -> str: month=day_date.month, day=day_date.day, ) - parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog")) + parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog")) return "".join(parts) @@ -282,7 +282,7 @@ def _day_nav_sx(ctx: dict) -> str: # Day admin header # --------------------------------------------------------------------------- -def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: +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") @@ -300,7 +300,7 @@ def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: month=day_date.month, day=day_date.day, ) - return sx_call("menu-row-sx", id="day-admin-row", level=5, + 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) @@ -309,7 +309,7 @@ def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: # Calendar admin header # --------------------------------------------------------------------------- -def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: +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") @@ -323,11 +323,11 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: ("calendar.admin.calendar_description_edit", "description"), ]: href = url_for(endpoint, calendar_slug=cal_slug) - nav_parts.append(sx_call("nav-link", href=href, label=label, + nav_parts.append(await render_to_sx("nav-link", href=href, label=label, select_colours=select_colours)) nav_html = "".join(nav_parts) - return sx_call("menu-row-sx", id="calendar-admin-row", level=4, + 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) @@ -336,13 +336,13 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: # Markets header # --------------------------------------------------------------------------- -def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str: +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 sx_call("menu-row-sx", id="markets-row", level=3, + return await render_to_sx("menu-row-sx", id="markets-row", level=3, link_href=link_href, - link_label_content=SxExpr(sx_call("events-markets-label")), + link_label_content=SxExpr(await render_to_sx("events-markets-label")), child_id="markets-header-child", oob=oob) @@ -350,7 +350,7 @@ def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str: # Calendars main panel # --------------------------------------------------------------------------- -def _calendars_main_panel_sx(ctx: dict) -> str: +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 {} @@ -365,18 +365,18 @@ def _calendars_main_panel_sx(ctx: dict) -> str: form_html = "" if can_create: create_url = url_for("calendars.create_calendar") - form_html = sx_call("crud-create-form", + 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 = _calendars_list_sx(ctx, calendars) - return sx_call("crud-panel", + 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") -def _calendars_list_sx(ctx: dict, calendars: list) -> str: +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 @@ -385,7 +385,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str: prefix = route_prefix() if not calendars: - return sx_call("empty-state", message="No calendars yet. Create one above.", + return await render_to_sx("empty-state", message="No calendars yet. Create one above.", cls="text-gray-500 mt-4") parts = [] @@ -395,7 +395,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str: href = prefix + url_for("calendar.get", calendar_slug=cal_slug) del_url = url_for("calendar.delete", calendar_slug=cal_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' - parts.append(sx_call("crud-item", + 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", @@ -408,7 +408,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str: # Calendar month grid # --------------------------------------------------------------------------- -def _calendar_main_panel_html(ctx: dict) -> str: +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 @@ -446,10 +446,10 @@ def _calendar_main_panel_html(ctx: dict) -> str: ("\u2039", prev_month_year, prev_month), ]: href = nav_link(yr, mn) - nav_arrows.append(sx_call("events-calendar-nav-arrow", + nav_arrows.append(await render_to_sx("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) - nav_arrows.append(sx_call("events-calendar-month-label", + nav_arrows.append(await render_to_sx("events-calendar-month-label", month_name=month_name, year=str(year))) for label, yr, mn in [ @@ -457,11 +457,14 @@ def _calendar_main_panel_html(ctx: dict) -> str: ("\u00bb", next_year, month), ]: href = nav_link(yr, mn) - nav_arrows.append(sx_call("events-calendar-nav-arrow", + nav_arrows.append(await render_to_sx("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) # Weekday headers - wd_html = "".join(sx_call("events-calendar-weekday", name=wd) for wd in weekday_names) + 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 = [] @@ -491,9 +494,9 @@ def _calendar_main_panel_html(ctx: dict) -> str: calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) - day_short_html = sx_call("events-calendar-day-short", + day_short_html = await render_to_sx("events-calendar-day-short", day_str=day_date.strftime("%a")) - day_num_html = sx_call("events-calendar-day-num", + day_num_html = await render_to_sx("events-calendar-day-num", pill_cls=pill_cls, href=day_href, num=str(day_date.day)) @@ -511,12 +514,12 @@ def _calendar_main_panel_html(ctx: dict) -> str: else: bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700" state_label = (e.state or "pending").replace("_", " ") - entry_badges.append(sx_call("events-calendar-entry-badge", + 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(sx_call("events-calendar-cell", + 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)) @@ -524,7 +527,7 @@ def _calendar_main_panel_html(ctx: dict) -> str: cells_html = "(<> " + "".join(cells) + ")" arrows_html = "(<> " + "".join(nav_arrows) + ")" wd_html = "(<> " + wd_html + ")" - return sx_call("events-calendar-grid", + return await render_to_sx("events-calendar-grid", arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html), cells=SxExpr(cells_html)) @@ -533,7 +536,7 @@ def _calendar_main_panel_html(ctx: dict) -> str: # Day main panel # --------------------------------------------------------------------------- -def _day_main_panel_html(ctx: dict) -> str: +async def _day_main_panel_html(ctx: dict) -> str: """Render the day entries table + add button.""" from quart import url_for @@ -554,9 +557,12 @@ def _day_main_panel_html(ctx: dict) -> str: rows_html = "" if day_entries: - rows_html = "".join(_day_row_html(ctx, entry) for entry in 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 = sx_call("events-day-empty-row") + rows_html = await render_to_sx("events-day-empty-row") add_url = url_for( "calendar.day.calendar_entries.add_form", @@ -564,12 +570,12 @@ def _day_main_panel_html(ctx: dict) -> str: day=day, month=month, year=year, ) - return sx_call("events-day-table", + return await render_to_sx("events-day-table", list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) -def _day_row_html(ctx: dict, entry) -> str: +async def _day_row_html(ctx: dict, entry) -> str: """Render a single day table row.""" from quart import url_for calendar = ctx.get("calendar") @@ -588,7 +594,7 @@ def _day_row_html(ctx: dict, entry) -> str: ) # Name - name_html = sx_call("events-day-row-name", + name_html = await render_to_sx("events-day-row-name", href=entry_href, pill_cls=pill_cls, name=entry.name) # Slot/Time @@ -597,44 +603,44 @@ def _day_row_html(ctx: dict, entry) -> str: slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id) time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" - slot_html = sx_call("events-day-row-slot", + 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 = sx_call("events-day-row-time", start=start, end=end) + slot_html = await render_to_sx("events-day-row-time", start=start, end=end) # State state = getattr(entry, "state", "pending") or "pending" - state_badge = _entry_state_badge_html(state) - state_td = sx_call("events-day-row-state", + state_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 = sx_call("events-day-row-cost", cost_str=cost_str) + 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 = sx_call("events-day-row-tickets", + tickets_td = await render_to_sx("events-day-row-tickets", price_str=f"\u00a3{tp:.2f}", count_str=tc_str) else: - tickets_td = sx_call("events-day-row-no-tickets") + tickets_td = await render_to_sx("events-day-row-no-tickets") - actions_td = sx_call("events-day-row-actions") + actions_td = await render_to_sx("events-day-row-actions") - return sx_call("events-day-row", + 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)) -def _entry_state_badge_html(state: str) -> str: +async def _entry_state_badge_html(state: str) -> str: """Render an entry state badge.""" state_classes = { "confirmed": "bg-emerald-100 text-emerald-800", @@ -645,23 +651,23 @@ def _entry_state_badge_html(state: str) -> str: } cls = state_classes.get(state, "bg-stone-100 text-stone-700") label = state.replace("_", " ").capitalize() - return sx_call("badge", cls=cls, label=label) + return await render_to_sx("badge", cls=cls, label=label) # --------------------------------------------------------------------------- # Day admin main panel # --------------------------------------------------------------------------- -def _day_admin_main_panel_html(ctx: dict) -> str: +async def _day_admin_main_panel_html(ctx: dict) -> str: """Render day admin panel (placeholder nav).""" - return sx_call("events-day-admin-panel") + return await render_to_sx("events-day-admin-panel") # --------------------------------------------------------------------------- # Calendar admin main panel # --------------------------------------------------------------------------- -def _calendar_admin_main_panel_html(ctx: dict) -> str: +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") @@ -674,17 +680,17 @@ def _calendar_admin_main_panel_html(ctx: dict) -> str: 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 = _calendar_description_display_html(calendar, desc_edit_url) + description_html = await _calendar_description_display_html(calendar, desc_edit_url) - return sx_call("events-calendar-admin-panel", + return await render_to_sx("events-calendar-admin-panel", description_content=SxExpr(description_html), csrf=csrf, description=desc) -def _calendar_description_display_html(calendar, edit_url: str) -> str: +async def _calendar_description_display_html(calendar, edit_url: str) -> str: """Render calendar description display with edit button.""" desc = getattr(calendar, "description", "") or "" - return sx_call("events-calendar-description-display", + return await render_to_sx("events-calendar-description-display", description=desc, edit_url=edit_url) @@ -692,7 +698,7 @@ def _calendar_description_display_html(calendar, edit_url: str) -> str: # Markets main panel # --------------------------------------------------------------------------- -def _markets_main_panel_html(ctx: dict) -> str: +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 {} @@ -706,18 +712,18 @@ def _markets_main_panel_html(ctx: dict) -> str: form_html = "" if can_create: create_url = url_for("markets.create_market") - form_html = sx_call("crud-create-form", + 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 = _markets_list_html(ctx, markets) - return sx_call("crud-panel", + 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") -def _markets_list_html(ctx: dict, markets: list) -> str: +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") @@ -726,7 +732,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str: slug = post.get("slug", "") if not markets: - return sx_call("empty-state", message="No markets yet. Create one above.", + return await render_to_sx("empty-state", message="No markets yet. Create one above.", cls="text-gray-500 mt-4") parts = [] @@ -736,7 +742,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str: market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/") del_url = url_for("markets.delete_market", market_slug=m_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' - parts.append(sx_call("crud-item", + 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, @@ -750,7 +756,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str: # Ticket state badge helper # --------------------------------------------------------------------------- -def _ticket_state_badge_html(state: str) -> str: +async def _ticket_state_badge_html(state: str) -> str: """Render a ticket state badge.""" cls_map = { "confirmed": "bg-emerald-100 text-emerald-800", @@ -760,14 +766,14 @@ def _ticket_state_badge_html(state: str) -> str: } cls = cls_map.get(state, "bg-stone-100 text-stone-700") label = (state or "").replace("_", " ").capitalize() - return sx_call("badge", cls=cls, label=label) + return await render_to_sx("badge", cls=cls, label=label) # --------------------------------------------------------------------------- # Tickets main panel (my tickets) # --------------------------------------------------------------------------- -def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: +async def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: """Render my tickets list.""" from quart import url_for @@ -787,16 +793,16 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: if entry.end_at: time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}" - ticket_cards.append(sx_call("events-ticket-card", + 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(_ticket_state_badge_html(state)), + badge=SxExpr(await _ticket_state_badge_html(state)), code_prefix=ticket.code[:8])) cards_html = "".join(ticket_cards) - return sx_call("events-tickets-panel", + return await render_to_sx("events-tickets-panel", list_container=_list_container(ctx), has_tickets=bool(tickets), cards=SxExpr(cards_html)) @@ -805,7 +811,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: # Ticket detail panel # --------------------------------------------------------------------------- -def _ticket_detail_panel_html(ctx: dict, ticket) -> str: +async def _ticket_detail_panel_html(ctx: dict, ticket) -> str: """Render a single ticket detail with QR code.""" from quart import url_for @@ -822,7 +828,7 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str: back_href = url_for("defpage_my_tickets") # Badge with larger sizing - badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm') + 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 @@ -841,7 +847,7 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str: "}})()" ) - return sx_call("events-ticket-detail", + 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, @@ -855,7 +861,7 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str: # Ticket admin main panel # --------------------------------------------------------------------------- -def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: +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") @@ -872,7 +878,7 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: ]: val = stats.get(key, 0) lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500" - stats_html += sx_call("events-ticket-admin-stat", + 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) @@ -886,29 +892,29 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: date_html = "" if entry and entry.start_at: - date_html = sx_call("events-ticket-admin-date", + 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 = sx_call("events-ticket-admin-checkin-form", + 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 = sx_call("events-ticket-admin-checked-in", + action_html = await render_to_sx("events-ticket-admin-checked-in", time_str=t_str) - rows_html += sx_call("events-ticket-admin-row", + 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(_ticket_state_badge_html(state)), + badge=SxExpr(await _ticket_state_badge_html(state)), action=SxExpr(action_html)) - return sx_call("events-ticket-admin-panel", + 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)) @@ -918,7 +924,7 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: # All events / page summary entry cards # --------------------------------------------------------------------------- -def _entry_card_html(entry, page_info: dict, pending_tickets: dict, +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.""" @@ -936,36 +942,36 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict, # Title (linked or plain) if entry_href: - title_html = sx_call("events-entry-title-linked", + title_html = await render_to_sx("events-entry-title-linked", href=entry_href, name=entry.name) else: - title_html = sx_call("events-entry-title-plain", name=entry.name) + 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 += sx_call("events-entry-page-badge", + 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 += sx_call("events-entry-cal-badge", name=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 += sx_call("events-entry-time-linked", + 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 += sx_call("events-entry-time-plain", + 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 = sx_call("events-entry-cost", + cost_html = await render_to_sx("events-entry-cost", cost=f"£{cost:.2f}") if cost else "" # Ticket widget @@ -973,16 +979,16 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict, widget_html = "" if tp is not None: qty = pending_tickets.get(entry.id, 0) - widget_html = sx_call("events-entry-widget-wrapper", - widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={}))) + widget_html = await render_to_sx("events-entry-widget-wrapper", + widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={}))) - return sx_call("events-entry-card", + 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)) -def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, +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.""" @@ -1000,27 +1006,27 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, # Title if entry_href: - title_html = sx_call("events-entry-title-tile-linked", + title_html = await render_to_sx("events-entry-title-tile-linked", href=entry_href, name=entry.name) else: - title_html = sx_call("events-entry-title-tile-plain", name=entry.name) + 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 += sx_call("events-entry-page-badge", + 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 += sx_call("events-entry-cal-badge", name=cal_name) + badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name) # Time time_html = "" if day_href: - time_html += sx_call("events-entry-time-linked", + time_html += (await render_to_sx("events-entry-time-linked", href=day_href, - date_str=entry.start_at.strftime("%a %-d %b")).replace(" · ", "") + 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")}' @@ -1028,7 +1034,7 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}' cost = getattr(entry, "cost", None) - cost_html = sx_call("events-entry-cost", + cost_html = await render_to_sx("events-entry-cost", cost=f"£{cost:.2f}") if cost else "" # Ticket widget @@ -1036,16 +1042,16 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, widget_html = "" if tp is not None: qty = pending_tickets.get(entry.id, 0) - widget_html = sx_call("events-entry-tile-widget-wrapper", - widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={}))) + widget_html = await render_to_sx("events-entry-tile-widget-wrapper", + widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={}))) - return sx_call("events-entry-card-tile", + 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)) -def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: +async def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: """Render the inline +/- ticket widget.""" csrf_token_val = "" if ctx: @@ -1062,26 +1068,26 @@ def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: tp = getattr(entry, "ticket_price", 0) or 0 tgt = f"#page-ticket-{eid}" - def _tw_form(count_val, btn_html): - return sx_call("events-tw-form", + 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 = _tw_form(1, sx_call("events-tw-cart-plus")) + inner = await _tw_form(1, await render_to_sx("events-tw-cart-plus")) else: - minus = _tw_form(qty - 1, sx_call("events-tw-minus")) - cart_icon = sx_call("events-tw-cart-icon", qty=str(qty)) - plus = _tw_form(qty + 1, sx_call("events-tw-plus")) + 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 sx_call("events-tw-widget", + return await render_to_sx("events-tw-widget", entry_id=str(eid), price=f"£{tp:.2f}", inner=SxExpr(inner)) -def _entry_cards_html(entries, page_info, pending_tickets, ticket_url, +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.""" @@ -1089,23 +1095,23 @@ def _entry_cards_html(entries, page_info, pending_tickets, ticket_url, last_date = None for entry in entries: if view == "tile": - parts.append(_entry_card_tile_html( + 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(sx_call("events-date-separator", + parts.append(await render_to_sx("events-date-separator", date_str=entry_date)) last_date = entry_date - parts.append(_entry_card_html( + 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(sx_call("sentinel-simple", + parts.append(await render_to_sx("sentinel-simple", id=f"sentinel-{page}", next_url=next_url)) return "".join(parts) @@ -1118,21 +1124,21 @@ _LIST_SVG = None _TILE_SVG = None -def _get_list_svg(): +async def _get_list_svg(): global _LIST_SVG if _LIST_SVG is None: - _LIST_SVG = sx_call("list-svg") + _LIST_SVG = await render_to_sx("list-svg") return _LIST_SVG -def _get_tile_svg(): +async def _get_tile_svg(): global _TILE_SVG if _TILE_SVG is None: - _TILE_SVG = sx_call("tile-svg") + _TILE_SVG = await render_to_sx("tile-svg") return _TILE_SVG -def _view_toggle_html(ctx: dict, view: str) -> str: +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() @@ -1151,34 +1157,34 @@ def _view_toggle_html(ctx: dict, view: str) -> str: list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' - return sx_call("view-toggle", + 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(_get_list_svg()), tile_svg=SxExpr(_get_tile_svg())) + list_svg=SxExpr(await _get_list_svg()), tile_svg=SxExpr(await _get_tile_svg())) -def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info, +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 = _view_toggle_html(ctx, view) + toggle = await _view_toggle_html(ctx, view) if entries: - cards = _entry_cards_html( + 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 = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards)) + body = await render_to_sx("events-grid", grid_cls=grid_cls, cards=SxExpr(cards)) else: - body = sx_call("empty-state", icon="fa fa-calendar-xmark", + 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 sx_call("events-main-panel-body", + return await render_to_sx("events-main-panel-body", toggle=SxExpr(toggle), body=SxExpr(body)) @@ -1212,12 +1218,12 @@ async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets, 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 = _events_main_panel_html( + content = await _events_main_panel_html( ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, ) - hdr = root_header_sx(ctx) - return full_page_sx(ctx, header_rows=hdr, content=content) + hdr = await root_header_sx(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, @@ -1231,11 +1237,11 @@ async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets, 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 = _events_main_panel_html( + content = await _events_main_panel_html( ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, ) - return oob_page_sx(content=content) + return await oob_page_sx(content=content) async def render_all_events_cards(entries, has_more, pending_tickets, @@ -1249,7 +1255,7 @@ async def render_all_events_cards(entries, has_more, pending_tickets, 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 _entry_cards_html( + return await _entry_cards_html( entries, page_info, pending_tickets, ticket_url, events_url, view, page, has_more, next_url, ) @@ -1271,15 +1277,15 @@ async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets 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 = _events_main_panel_html( + 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 = root_header_sx(ctx) - hdr += header_child_sx(_post_header_sx(ctx)) - return full_page_sx(ctx, header_rows=hdr, content=content) + hdr = await root_header_sx(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, @@ -1294,15 +1300,15 @@ async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets, 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 = _events_main_panel_html( + 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 = _post_header_sx(ctx, oob=True) + oobs = await _post_header_sx(ctx, oob=True) oobs += _clear_deeper_oob("post-row", "post-header-child") - return oob_page_sx(oobs=oobs, content=content) + return await oob_page_sx(oobs=oobs, content=content) async def render_page_summary_cards(entries, has_more, pending_tickets, @@ -1316,7 +1322,7 @@ async def render_page_summary_cards(entries, has_more, pending_tickets, 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 _entry_cards_html( + 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, @@ -1329,24 +1335,24 @@ async def render_page_summary_cards(entries, has_more, pending_tickets, async def render_calendars_page(ctx: dict) -> str: """Full page: calendars listing.""" - content = _calendars_main_panel_sx(ctx) + content = await _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sx(ctx) - post_hdr = _post_header_sx(ctx) - admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") - return full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content) + root_hdr = await root_header_sx(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 = _calendars_main_panel_sx(ctx) + content = await _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + 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 oob_page_sx(oobs=oobs, content=content) + return await oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1355,22 +1361,22 @@ async def render_calendars_oob(ctx: dict) -> str: async def render_calendar_page(ctx: dict) -> str: """Full page: calendar month view.""" - content = _calendar_main_panel_html(ctx) - hdr = root_header_sx(ctx) - child = _post_header_sx(ctx) + _calendar_header_sx(ctx) - hdr += header_child_sx(child) - return full_page_sx(ctx, header_rows=hdr, content=content) + content = await _calendar_main_panel_html(ctx) + hdr = await root_header_sx(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 = _calendar_main_panel_html(ctx) - oobs = _post_header_sx(ctx, oob=True) - oobs += oob_header_sx("post-header-child", "calendar-header-child", - _calendar_header_sx(ctx)) + 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 oob_page_sx(oobs=oobs, content=content) + return await oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1379,35 +1385,35 @@ async def render_calendar_oob(ctx: dict) -> str: async def render_day_page(ctx: dict) -> str: """Full page: day detail.""" - content = _day_main_panel_html(ctx) - hdr = root_header_sx(ctx) - child = (_post_header_sx(ctx) - + _calendar_header_sx(ctx) + _day_header_sx(ctx)) - hdr += header_child_sx(child) - return full_page_sx(ctx, header_rows=hdr, content=content) + content = await _day_main_panel_html(ctx) + hdr = await root_header_sx(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 = _day_main_panel_html(ctx) - oobs = _calendar_header_sx(ctx, oob=True) - oobs += oob_header_sx("calendar-header-child", "day-header-child", - _day_header_sx(ctx)) + 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 oob_page_sx(oobs=oobs, content=content) + return await oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Calendar admin helper # --------------------------------------------------------------------------- -def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False, +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 post_admin_header_sx(ctx, slug, oob=oob, selected=selected) + return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected) # =========================================================================== @@ -1419,19 +1425,19 @@ def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False, # Ticket widget (public wrapper for _ticket_widget_html) # --------------------------------------------------------------------------- -def render_ticket_widget(entry, qty: int, ticket_url: str) -> str: +async def render_ticket_widget(entry, qty: int, ticket_url: str) -> str: """Render the +/- ticket widget for page_summary / all_events adjust_ticket.""" - return _ticket_widget_html(entry, qty, ticket_url, ctx={}) + return await _ticket_widget_html(entry, qty, ticket_url, ctx={}) # --------------------------------------------------------------------------- # Ticket admin: checkin result # --------------------------------------------------------------------------- -def render_checkin_result(success: bool, error: str | None, ticket) -> str: +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 sx_call("events-checkin-error", + return await render_to_sx("events-checkin-error", message=error or "Check-in failed") if not ticket: return "" @@ -1443,15 +1449,15 @@ def render_checkin_result(success: bool, error: str | None, ticket) -> str: date_html = "" if entry and entry.start_at: - date_html = sx_call("events-ticket-admin-date", + date_html = await render_to_sx("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) - return sx_call("events-checkin-success-row", + 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(_ticket_state_badge_html("checked_in")), + badge=SxExpr(await _ticket_state_badge_html("checked_in")), time_str=time_str) @@ -1459,13 +1465,13 @@ def render_checkin_result(success: bool, error: str | None, ticket) -> str: # Ticket admin: lookup result # --------------------------------------------------------------------------- -def render_lookup_result(ticket, error: str | None) -> str: +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 sx_call("events-lookup-error", message=error) + return await render_to_sx("events-lookup-error", message=error) if not ticket: return "" @@ -1477,34 +1483,34 @@ def render_lookup_result(ticket, error: str | None) -> str: csrf = generate_csrf_token() # Info section - info_html = sx_call("events-lookup-info", + info_html = await render_to_sx("events-lookup-info", entry_name=entry.name if entry else "Unknown event") if tt: - info_html += sx_call("events-lookup-type", type_name=tt.name) + info_html += await render_to_sx("events-lookup-type", type_name=tt.name) if entry and entry.start_at: - info_html += sx_call("events-lookup-date", + 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 += sx_call("events-lookup-cal", cal_name=cal.name) - info_html += sx_call("events-lookup-status", - badge=SxExpr(_ticket_state_badge_html(state)), code=code) + 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 += sx_call("events-lookup-checkin-time", + 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 = sx_call("events-lookup-checkin-btn", + action_html = await render_to_sx("events-lookup-checkin-btn", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": - action_html = sx_call("events-lookup-checked-in") + action_html = await render_to_sx("events-lookup-checked-in") elif state == "cancelled": - action_html = sx_call("events-lookup-cancelled") + action_html = await render_to_sx("events-lookup-cancelled") - return sx_call("events-lookup-card", + return await render_to_sx("events-lookup-card", info=SxExpr(info_html), code=code, action=SxExpr(action_html)) @@ -1512,7 +1518,7 @@ def render_lookup_result(ticket, error: str | None) -> str: # Ticket admin: entry tickets table # --------------------------------------------------------------------------- -def render_entry_tickets_admin(entry, tickets: list) -> str: +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 @@ -1531,26 +1537,26 @@ def render_entry_tickets_admin(entry, tickets: list) -> str: action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = sx_call("events-entry-tickets-admin-checkin", + 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 = sx_call("events-ticket-admin-checked-in", + action_html = await render_to_sx("events-ticket-admin-checked-in", time_str=t_str) - rows_html += sx_call("events-entry-tickets-admin-row", + 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(_ticket_state_badge_html(state)), + badge=SxExpr(await _ticket_state_badge_html(state)), action=SxExpr(action_html)) if tickets: - body_html = sx_call("events-entry-tickets-admin-table", + body_html = await render_to_sx("events-entry-tickets-admin-table", rows=SxExpr(rows_html)) else: - body_html = sx_call("events-entry-tickets-admin-empty") + body_html = await render_to_sx("events-entry-tickets-admin-empty") - return sx_call("events-entry-tickets-admin-panel", + return await render_to_sx("events-entry-tickets-admin-panel", entry_name=entry.name, count_label=f"{count} ticket{suffix}", body=SxExpr(body_html)) @@ -1560,16 +1566,16 @@ def render_entry_tickets_admin(entry, tickets: list) -> str: # Day main panel -- public API # --------------------------------------------------------------------------- -def render_day_main_panel(ctx: dict) -> str: +async def render_day_main_panel(ctx: dict) -> str: """Public wrapper for day main panel rendering.""" - return _day_main_panel_html(ctx) + return await _day_main_panel_html(ctx) # --------------------------------------------------------------------------- # Entry main panel # --------------------------------------------------------------------------- -def _entry_main_panel_html(ctx: dict) -> str: +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 @@ -1590,63 +1596,63 @@ def _entry_main_panel_html(ctx: dict) -> str: eid = entry.id state = getattr(entry, "state", "pending") or "pending" - def _field(label, content_html): - return sx_call("events-entry-field", label=label, content=SxExpr(content_html)) + async def _field(label, content_html): + return await render_to_sx("events-entry-field", label=label, content=SxExpr(content_html)) # Name - name_html = _field("Name", sx_call("events-entry-name-field", name=entry.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 = sx_call("events-entry-slot-assigned", + slot_inner = await render_to_sx("events-entry-slot-assigned", slot_name=slot.name, flex_label=flex_label) else: - slot_inner = sx_call("events-entry-slot-none") - slot_html = _field("Slot", slot_inner) + 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 = _field("Time Period", sx_call("events-entry-time-field", + time_html = await _field("Time Period", await render_to_sx("events-entry-time-field", time_str=start_str + end_str)) # State - state_html = _field("State", sx_call("events-entry-state-field", + state_html = await _field("State", await render_to_sx("events-entry-state-field", entry_id=str(eid), - badge=SxExpr(_entry_state_badge_html(state)))) + 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 = _field("Cost", sx_call("events-entry-cost-field", + cost_html = await _field("Cost", await render_to_sx("events-entry-cost-field", cost=f"£{cost_str}")) # Ticket Configuration (admin) - tickets_html = _field("Tickets", sx_call("events-entry-tickets-field", + tickets_html = await _field("Tickets", await render_to_sx("events-entry-tickets-field", entry_id=str(eid), - tickets_config=SxExpr(render_entry_tickets_config(entry, calendar, day, month, year)))) + 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 = render_buy_form( + 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 = _field("Date", sx_call("events-entry-date-field", date_str=date_str)) + 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 = _field("Associated Posts", sx_call("events-entry-posts-field", + posts_html = await _field("Associated Posts", await render_to_sx("events-entry-posts-field", entry_id=str(eid), - posts_panel=SxExpr(render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))) + posts_panel=SxExpr(await render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))) # Options and Edit Button edit_url = url_for( @@ -1655,14 +1661,14 @@ def _entry_main_panel_html(ctx: dict) -> str: day=day, month=month, year=year, ) - return sx_call("events-entry-panel", + 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(_entry_options_html(entry, calendar, day, month, year)), + options=SxExpr(await _entry_options_html(entry, calendar, day, month, year)), pre_action=pre_action, edit_url=edit_url) @@ -1670,7 +1676,7 @@ def _entry_main_panel_html(ctx: dict) -> str: # Entry header row # --------------------------------------------------------------------------- -def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: +async def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: """Build entry detail header row.""" from quart import url_for @@ -1691,19 +1697,19 @@ def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: year=year, month=month, day=day, entry_id=entry.id, ) - label_html = sx_call("events-entry-label", + label_html = await render_to_sx("events-entry-label", entry_id=str(entry.id), - title=SxExpr(_entry_title_html(entry)), - times=SxExpr(_entry_times_html(entry))) + title=SxExpr(await _entry_title_html(entry)), + times=SxExpr(await _entry_times_html(entry))) - nav_html = _entry_nav_html(ctx) + nav_html = await _entry_nav_html(ctx) - return sx_call("menu-row-sx", id="entry-row", level=5, + 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) -def _entry_times_html(entry) -> str: +async def _entry_times_html(entry) -> str: """Render entry times label.""" start = entry.start_at end = entry.end_at @@ -1711,14 +1717,14 @@ def _entry_times_html(entry) -> str: return "" start_str = start.strftime("%H:%M") end_str = f" \u2192 {end.strftime('%H:%M')}" if end else "" - return sx_call("events-entry-times", time_str=start_str + end_str) + return await render_to_sx("events-entry-times", time_str=start_str + end_str) # --------------------------------------------------------------------------- # Entry nav (desktop + admin link) # --------------------------------------------------------------------------- -def _entry_nav_html(ctx: dict) -> str: +async def _entry_nav_html(ctx: dict) -> str: """Entry desktop nav: associated posts scrolling menu + admin link.""" from quart import url_for @@ -1749,13 +1755,13 @@ def _entry_nav_html(ctx: dict) -> str: feat = getattr(ep, "feature_image", None) href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" if feat: - img_html = sx_call("events-post-img", src=feat, alt=title) + img_html = await render_to_sx("events-post-img", src=feat, alt=title) else: - img_html = sx_call("events-post-img-placeholder") - post_links += sx_call("events-entry-nav-post-link", + 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(sx_call("events-entry-posts-nav-oob", - items=SxExpr(post_links)).replace(' :hx-swap-oob "true"', '')) + 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: @@ -1765,7 +1771,7 @@ def _entry_nav_html(ctx: dict) -> str: day=day, month=month, year=year, entry_id=entry.id, ) - parts.append(sx_call("events-entry-admin-link", href=admin_url)) + parts.append(await render_to_sx("events-entry-admin-link", href=admin_url)) return "".join(parts) @@ -1774,26 +1780,26 @@ def _entry_nav_html(ctx: dict) -> str: # Entry optioned (confirm/decline/provisional response) # --------------------------------------------------------------------------- -def render_entry_optioned(entry, calendar, day, month, year) -> str: +async def render_entry_optioned(entry, calendar, day, month, year) -> str: """Render entry options buttons + OOB title & state swaps.""" - options = _entry_options_html(entry, calendar, day, month, year) - title = _entry_title_html(entry) - state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending") + 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 + sx_call("events-entry-optioned-oob", + return options + await render_to_sx("events-entry-optioned-oob", entry_id=str(entry.id), title=SxExpr(title), state=SxExpr(state)) -def _entry_title_html(entry) -> str: +async def _entry_title_html(entry) -> str: """Render entry title (icon + name + state badge).""" state = getattr(entry, "state", "pending") or "pending" - return sx_call("events-entry-title", + return await render_to_sx("events-entry-title", name=entry.name, - badge=SxExpr(_entry_state_badge_html(state))) + badge=SxExpr(await _entry_state_badge_html(state))) -def _entry_options_html(entry, calendar, day, month, year) -> str: +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 @@ -1807,13 +1813,13 @@ def _entry_options_html(entry, calendar, day, month, year) -> str: state = getattr(entry, "state", "pending") or "pending" target = f"#calendar_entry_options_{eid}" - def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"): + 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 sx_call("events-entry-option-button", + 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, @@ -1821,22 +1827,22 @@ def _entry_options_html(entry, calendar, day, month, year) -> str: buttons_html = "" if state == "provisional": - buttons_html += _make_button( + buttons_html += await _make_button( "confirm_entry", "confirm", "Confirm entry?", "Are you sure you want to confirm this entry?", ) - buttons_html += _make_button( + buttons_html += await _make_button( "decline_entry", "decline", "Decline entry?", "Are you sure you want to decline this entry?", ) elif state == "confirmed": - buttons_html += _make_button( + buttons_html += await _make_button( "provisional_entry", "provisional", "Provisional entry?", "Are you sure you want to provisional this entry?", trigger_type="button", ) - return sx_call("events-entry-options", + return await render_to_sx("events-entry-options", entry_id=str(eid), buttons=SxExpr(buttons_html)) @@ -1844,7 +1850,7 @@ def _entry_options_html(entry, calendar, day, month, year) -> str: # Entry tickets config (display + form) # --------------------------------------------------------------------------- -def render_entry_tickets_config(entry, calendar, day, month, year) -> str: +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 @@ -1861,11 +1867,11 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str: if tp is not None: tc_str = f"{tc} tickets" if tc is not None else "Unlimited" - display_html = sx_call("events-ticket-config-display", + 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 = sx_call("events-ticket-config-none", show_js=show_js) + 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", @@ -1875,7 +1881,7 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str: tp_val = f"{tp:.2f}" if tp is not None else "" tc_val = str(tc) if tc is not None else "" - form_html = sx_call("events-ticket-config-form", + 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) @@ -1886,7 +1892,7 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str: # Entry posts panel # --------------------------------------------------------------------------- -def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str: +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 @@ -1903,28 +1909,28 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> ep_title = getattr(ep, "title", "") ep_id = getattr(ep, "id", 0) feat = getattr(ep, "feature_image", None) - img_html = (sx_call("events-post-img", src=feat, alt=ep_title) - if feat else sx_call("events-post-img-placeholder")) + 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 += sx_call("events-entry-post-item", + 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 = sx_call("events-entry-posts-list", items=SxExpr(items)) + posts_html = await render_to_sx("events-entry-posts-list", items=SxExpr(items)) else: - posts_html = sx_call("events-entry-posts-none") + 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 sx_call("events-entry-posts-panel", + return await render_to_sx("events-entry-posts-panel", posts=SxExpr(posts_html), search_url=search_url, entry_id=eid_s) @@ -1933,7 +1939,7 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> # Entry posts nav OOB # --------------------------------------------------------------------------- -def render_entry_posts_nav_oob(entry_posts) -> str: +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 {} @@ -1941,7 +1947,7 @@ def render_entry_posts_nav_oob(entry_posts) -> str: blog_url_fn = getattr(g, "blog_url", None) if not entry_posts: - return sx_call("events-entry-posts-nav-oob-empty") + return await render_to_sx("events-entry-posts-nav-oob-empty") items = "" for ep in entry_posts: @@ -1949,20 +1955,20 @@ def render_entry_posts_nav_oob(entry_posts) -> str: title = getattr(ep, "title", "") feat = getattr(ep, "feature_image", None) href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" - img_html = (sx_call("events-post-img", src=feat, alt=title) - if feat else sx_call("events-post-img-placeholder")) - items += sx_call("events-entry-nav-post", + 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 sx_call("events-entry-posts-nav-oob", items=SxExpr(items)) + return await render_to_sx("events-entry-posts-nav-oob", items=SxExpr(items)) # --------------------------------------------------------------------------- # Day entries nav OOB # --------------------------------------------------------------------------- -def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: +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 @@ -1971,7 +1977,7 @@ def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: cal_slug = getattr(calendar, "slug", "") if not confirmed_entries: - return sx_call("events-day-entries-nav-oob-empty") + return await render_to_sx("events-day-entries-nav-oob-empty") items = "" for entry in confirmed_entries: @@ -1983,18 +1989,18 @@ def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - items += sx_call("events-day-nav-entry", + items += await render_to_sx("events-day-nav-entry", href=href, nav_btn=nav_btn, name=entry.name, time_str=start + end) - return sx_call("events-day-entries-nav-oob", items=SxExpr(items)) + return await render_to_sx("events-day-entries-nav-oob", items=SxExpr(items)) # --------------------------------------------------------------------------- # Post nav entries OOB # --------------------------------------------------------------------------- -def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: +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 @@ -2006,7 +2012,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: has_items = has_entries or calendars if not has_items: - return sx_call("events-post-nav-oob-empty") + return await render_to_sx("events-post-nav-oob-empty") slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "") @@ -2021,7 +2027,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: href = events_url(entry_path) time_str = entry.start_at.strftime("%b %d, %Y at %H:%M") end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - items += sx_call("events-post-nav-entry", + items += await render_to_sx("events-post-nav-entry", href=href, nav_btn=nav_btn, name=entry.name, time_str=time_str + end_str) @@ -2029,7 +2035,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: for cal in calendars: cs = getattr(cal, "slug", "") local_href = events_url(f"/{slug}/{cs}/") - items += sx_call("events-post-nav-calendar", + items += await render_to_sx("events-post-nav-calendar", href=local_href, nav_btn=nav_btn, name=cal.name) hs = ("on load or scroll " @@ -2037,7 +2043,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: "remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow " "else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end") - return sx_call("events-post-nav-wrapper", + return await render_to_sx("events-post-nav-wrapper", items=SxExpr(items), hyperscript=hs) @@ -2045,22 +2051,22 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: # Calendar description display + edit form # --------------------------------------------------------------------------- -def render_calendar_description(calendar, *, oob: bool = False) -> str: +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 = _calendar_description_display_html(calendar, edit_url) + html = await _calendar_description_display_html(calendar, edit_url) if oob: desc = getattr(calendar, "description", "") or "" - html += sx_call("events-calendar-description-title-oob", + html += await render_to_sx("events-calendar-description-title-oob", description=desc) return html -def render_calendar_description_edit(calendar) -> str: +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 @@ -2071,7 +2077,7 @@ def render_calendar_description_edit(calendar) -> str: 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 sx_call("events-calendar-description-edit-form", + return await render_to_sx("events-calendar-description-edit-form", save_url=save_url, cancel_url=cancel_url, csrf=csrf, description=desc) @@ -2080,25 +2086,25 @@ def render_calendar_description_edit(calendar) -> str: # Calendars list panel (for POST create / DELETE) # --------------------------------------------------------------------------- -def render_calendars_list_panel(ctx: dict) -> str: +async def render_calendars_list_panel(ctx: dict) -> str: """Render the calendars main panel HTML for POST/DELETE response.""" - return _calendars_main_panel_sx(ctx) + return await _calendars_main_panel_sx(ctx) # --------------------------------------------------------------------------- # Markets list panel (for POST create / DELETE) # --------------------------------------------------------------------------- -def render_markets_list_panel(ctx: dict) -> str: +async def render_markets_list_panel(ctx: dict) -> str: """Render the markets main panel HTML for POST/DELETE response.""" - return _markets_main_panel_html(ctx) + return await _markets_main_panel_html(ctx) # --------------------------------------------------------------------------- # Slot main panel # --------------------------------------------------------------------------- -def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: +async def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: """Render slot detail view.""" from quart import url_for, g @@ -2121,15 +2127,15 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: # Days pills if days and days[0] != "\u2014": days_inner = "".join( - sx_call("events-slot-day-pill", day=d) for d in days + await render_to_sx("events-slot-day-pill", day=d) for d in days ) - days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner)) + days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner)) else: - days_html = sx_call("events-slot-no-days") + days_html = await render_to_sx("events-slot-no-days") sid = str(slot.id) - result = sx_call("events-slot-panel", + 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", @@ -2138,7 +2144,7 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: pre_action=pre_action, edit_url=edit_url) if oob: - result += sx_call("events-slot-description-oob", description=desc) + result += await render_to_sx("events-slot-description-oob", description=desc) return result @@ -2147,7 +2153,7 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: # Slots table # --------------------------------------------------------------------------- -def render_slots_table(slots, calendar) -> str: +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 @@ -2173,18 +2179,18 @@ def render_slots_table(slots, calendar) -> str: day_list = days_display.split(", ") if day_list and day_list[0] != "\u2014": days_inner = "".join( - sx_call("events-slot-day-pill", day=d) for d in day_list + await render_to_sx("events-slot-day-pill", day=d) for d in day_list ) - days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner)) + days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner)) else: - days_html = sx_call("events-slot-no-days") + 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 += sx_call("events-slots-row", + 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, @@ -2195,11 +2201,11 @@ def render_slots_table(slots, calendar) -> str: del_url=del_url, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') else: - rows_html = sx_call("events-slots-empty-row") + rows_html = await render_to_sx("events-slots-empty-row") add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug) - return sx_call("events-slots-table", + return await render_to_sx("events-slots-table", list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) @@ -2208,7 +2214,7 @@ def render_slots_table(slots, calendar) -> str: # Ticket type main panel # --------------------------------------------------------------------------- -def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str: +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 @@ -2228,14 +2234,14 @@ def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year year=year, month=month, day=day, entry_id=entry.id, ) - def _col(label, val): - return sx_call("events-ticket-type-col", label=label, value=val) + async def _col(label, val): + return await render_to_sx("events-ticket-type-col", label=label, value=val) - return sx_call("events-ticket-type-panel", + return await render_to_sx("events-ticket-type-panel", ticket_id=tid, list_container=list_container, - c1=_col("Name", ticket_type.name), - c2=_col("Cost", cost_str), - c3=_col("Count", str(count)), + 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) @@ -2243,7 +2249,7 @@ def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year # Ticket types table # --------------------------------------------------------------------------- -def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str: +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 @@ -2274,7 +2280,7 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) - cost = getattr(tt, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" - rows_html += sx_call("events-ticket-types-row", + 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, @@ -2282,14 +2288,14 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) - del_url=del_url, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') else: - rows_html = sx_call("events-ticket-types-empty-row") + 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 sx_call("events-ticket-types-table", + 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) @@ -2298,11 +2304,11 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) - # Buy result (ticket purchase confirmation) # --------------------------------------------------------------------------- -def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: +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 = _cart_icon_oob(cart_count) + cart_html = await _cart_icon_oob(cart_count) count = len(created_tickets) suffix = "s" if count != 1 else "" @@ -2310,18 +2316,18 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: tickets_html = "" for ticket in created_tickets: href = url_for("defpage_ticket_detail", code=ticket.code) - tickets_html += sx_call("events-buy-result-ticket", + 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 = sx_call("events-buy-result-remaining", + 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 + sx_call("events-buy-result", + 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), @@ -2333,7 +2339,7 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: # Buy form (ticket +/- controls) # --------------------------------------------------------------------------- -def render_buy_form(entry, ticket_remaining, ticket_sold_count, +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 @@ -2350,7 +2356,7 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, return "" if state != "confirmed": - return sx_call("events-buy-not-confirmed", entry_id=eid_s) + 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}" @@ -2359,16 +2365,16 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, info_html = "" info_items = "" if ticket_sold_count: - info_items += sx_call("events-buy-info-sold", + info_items += await render_to_sx("events-buy-info-sold", count=str(ticket_sold_count)) if ticket_remaining is not None: - info_items += sx_call("events-buy-info-remaining", + info_items += await render_to_sx("events-buy-info-remaining", count=str(ticket_remaining)) if user_ticket_count: - info_items += sx_call("events-buy-info-basket", + info_items += await render_to_sx("events-buy-info-basket", count=str(user_ticket_count)) if info_items: - info_html = sx_call("events-buy-info-bar", items=SxExpr(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] @@ -2378,46 +2384,46 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, for tt in active_types: type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0 cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00" - type_items += sx_call("events-buy-type-item", + type_items += await render_to_sx("events-buy-type-item", type_name=tt.name, cost_str=cost_str, - adjust_controls=SxExpr(_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id))) - body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items)) + 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 = sx_call("events-buy-default", + body_html = await render_to_sx("events-buy-default", price_str=f"\u00a3{tp:.2f}", - adjust_controls=SxExpr(_ticket_adjust_controls(csrf, adjust_url, target, eid, qty))) + adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, qty))) - return sx_call("events-buy-panel", + return await render_to_sx("events-buy-panel", entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html)) -def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None): +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 = sx_call("events-adjust-tt-hidden", + 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) - def _adj_form(count_val, btn_html, *, extra_cls=""): - return sx_call("events-adjust-form", + 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 _adj_form(1, sx_call("events-adjust-cart-plus"), + 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 = _adj_form(count - 1, sx_call("events-adjust-minus")) - cart_icon = sx_call("events-adjust-cart-icon", + 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 = _adj_form(count + 1, sx_call("events-adjust-plus")) + plus = await _adj_form(count + 1, await render_to_sx("events-adjust-plus")) - return sx_call("events-adjust-controls", + return await render_to_sx("events-adjust-controls", minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus)) @@ -2425,19 +2431,19 @@ def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket # Adjust response (OOB cart icon + buy form) # --------------------------------------------------------------------------- -def render_adjust_response(entry, ticket_remaining, ticket_sold_count, +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 = _cart_icon_oob(cart_count) - form_html = render_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 -def _cart_icon_oob(count: int) -> str: +async def _cart_icon_oob(count: int) -> str: """Render the OOB cart icon/badge swap.""" from quart import g @@ -2451,11 +2457,11 @@ def _cart_icon_oob(count: int) -> str: if count == 0: blog_href = blog_url_fn("/") if blog_url_fn else "/" - return sx_call("events-cart-icon-logo", + 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 sx_call("events-cart-icon-badge", + return await render_to_sx("events-cart-icon-badge", cart_href=cart_href, count=str(count)) @@ -2564,7 +2570,7 @@ _SLOT_PICKER_JS = """\ # Entry edit form # =========================================================================== -def _slot_options_html(day_slots, selected_slot_id=None) -> str: +async def _slot_options_html(day_slots, selected_slot_id=None) -> str: """Build slot