Add macros, declarative handlers (defhandler), and convert all fragment routes to sx
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

Phase 1 — Macros: defmacro + quasiquote syntax (`, ,, ,@) in parser,
evaluator, HTML renderer, and JS mirror. Macro type, expansion, and
round-trip serialization.

Phase 2 — Expanded primitives: app-url, url-for, asset-url, config,
format-date, parse-int (pure); service, request-arg, request-path,
nav-tree, get-children (I/O); jinja-global, relations-from (pure).
Updated _io_service to accept (service "registry-name" "method" :kwargs)
with auto kebab→snake conversion. DTO-to-dict now expands datetime fields
into year/month/day convenience keys. Tuple returns converted to lists.

Phase 3 — Declarative handlers: HandlerDef type, defhandler special form,
handler registry (service → name → HandlerDef), async evaluator+renderer
(async_eval.py) that awaits I/O primitives inline within control flow.
Handler loading from .sx files, execute_handler, blueprint factory.

Phase 4 — Convert all fragment routes: 13 Python fragment handlers across
8 services replaced with declarative .sx handler files. All routes.py
simplified to uniform sx dispatch pattern. Two Jinja HTML handlers
(events/container-cards, events/account-page) kept as Python.

New files: shared/sx/async_eval.py, shared/sx/handlers.py,
shared/sx/tests/test_handlers.py, plus 13 handler .sx files under
{service}/sx/handlers/. MarketService.product_by_slug() added.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 00:22:18 +00:00
parent 13bcf755f6
commit ab75e505a8
48 changed files with 2538 additions and 638 deletions

View File

@@ -0,0 +1,19 @@
;; Events account-nav-item fragment handler
;;
;; Renders tickets + bookings links for the account dashboard nav.
(defhandler account-nav-item (&key)
(let ((styles (or (jinja-global "styles") (dict)))
(nav-class (or (get styles "nav_button") ""))
(hx-select "#main-panel, #search-mobile, #search-count-mobile, #search-desktop, #search-count-desktop, #menu-items-nav-wrapper"))
(<>
(~nav-group-link
:href (app-url "account" "/tickets/")
:hx-select hx-select
:nav-class nav-class
:label "tickets")
(~nav-group-link
:href (app-url "account" "/bookings/")
:hx-select hx-select
:nav-class nav-class
:label "bookings"))))

View File

@@ -0,0 +1,81 @@
;; Events container-nav fragment handler
;;
;; Renders calendar entry nav items + calendar link nav items
;; for the scrollable navigation panel on blog post pages.
;;
;; Params (from request.args):
;; container_type — "page" (default)
;; container_id — int
;; post_slug — string
;; paginate_url — base URL for infinite scroll
;; page — current page (default 1)
;; exclude — comma-separated exclusion prefixes
;; current_calendar — currently selected calendar slug
(defhandler container-nav
(&key container_type container_id post_slug paginate_url page exclude current_calendar)
(let ((ct (or container_type "page"))
(cid (parse-int (or container_id "0")))
(slug (or post_slug ""))
(purl (or paginate_url ""))
(pg (parse-int (or page "1")))
(excl-raw (or exclude ""))
(cur-cal (or current_calendar ""))
(excludes (filter (fn (e) (not (empty? e)))
(map trim (split excl-raw ","))))
(has-cal-excl (not (empty? (filter (fn (e) (starts-with? e "calendar"))
excludes))))
(styles (or (jinja-global "styles") (dict)))
(nav-class (or (get styles "nav_button") ""))
(sel-colours (or (jinja-global "select_colours") "")))
;; Only render if no calendar-* exclusion
(when (not has-cal-excl)
(let ((result (service "calendar" "associated-entries"
:content-type ct :content-id cid :page pg))
(entries (first result))
(has-more (nth result 1))
(calendars (service "calendar" "calendars-for-container"
:container-type ct :container-id cid)))
(<>
;; Calendar entry nav items
(map (fn (entry)
(let ((entry-path (str "/" slug "/"
(get entry "calendar_slug") "/"
(get entry "start_at_year") "/"
(get entry "start_at_month") "/"
(get entry "start_at_day")
"/entries/" (get entry "id") "/"))
(date-str (str (format-date (get entry "start_at") "%b %d, %Y at %H:%M")
(if (get entry "end_at")
(str " " (format-date (get entry "end_at") "%H:%M"))
""))))
(~calendar-entry-nav
:href (app-url "events" entry-path)
:name (get entry "name")
:date-str date-str
:nav-class nav-class))) entries)
;; Infinite scroll sentinel
(when (and has-more (not (empty? purl)))
(~htmx-sentinel
:id (str "entries-load-sentinel-" pg)
:hx-get (str purl "?page=" (+ pg 1))
:hx-trigger "intersect once"
:hx-swap "beforebegin"
:class "flex-shrink-0 w-1"))
;; Calendar link nav items
(map (fn (cal)
(let ((href (app-url "events" (str "/" slug "/" (get cal "slug") "/")))
(is-selected (if (not (empty? cur-cal))
(= (get cal "slug") cur-cal)
false)))
(~calendar-link-nav
:href href
:name (get cal "name")
:nav-class nav-class
:is-selected is-selected
:select-colours sel-colours))) calendars))))))

View File

@@ -0,0 +1,34 @@
;; Events link-card fragment handler
;;
;; Renders event page preview card(s) by slug.
;; Supports single mode (?slug=x) and batch mode (?keys=x,y,z).
(defhandler link-card (&key slug keys)
(if keys
(let ((slugs (filter (fn (s) (not (empty? s)))
(map trim (split keys ",")))))
(<> (map (fn (s)
(let ((post (query "blog" "post-by-slug" :slug s)))
(<> (str "<!-- fragment:" s " -->")
(when post
(let ((calendars (service "calendar" "calendars-for-container"
:container-type "page"
:container-id (get post "id")))
(cal-names (join ", " (map (fn (c) (get c "name")) calendars))))
(~link-card
:title (get post "title")
:image (get post "feature_image")
:subtitle cal-names
:link (app-url "events" (str "/" (get post "slug"))))))))) slugs)))
(when slug
(let ((post (query "blog" "post-by-slug" :slug slug)))
(when post
(let ((calendars (service "calendar" "calendars-for-container"
:container-type "page"
:container-id (get post "id")))
(cal-names (join ", " (map (fn (c) (get c "name")) calendars))))
(~link-card
:title (get post "title")
:image (get post "feature_image")
:subtitle cal-names
:link (app-url "events" (str "/" (get post "slug"))))))))))

View File

@@ -21,8 +21,9 @@ from shared.sx.helpers import (
)
from shared.sx.parser import SxExpr
# Load events-specific .sx components at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)))
# Load events-specific .sx components + handlers at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)),
service_name="events")
# ---------------------------------------------------------------------------