Refactor SX templates: shared components, Python migration, cleanup
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m0s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m0s
- Extract shared components (empty-state, delete-btn, sentinel, crud-*, view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth forms, order tables/detail/checkout) - Migrate all Python sx_call() callers to use shared components directly - Remove 55+ thin wrapper defcomps from domain .sx files - Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc) - Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx - Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx - Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0) - Add SX response validation and debug headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,12 +47,14 @@ def register():
|
||||
if not markets:
|
||||
return ""
|
||||
styles = current_app.jinja_env.globals.get("styles", {})
|
||||
nav_class = styles.get("nav_button_less_pad", "")
|
||||
nav_class = styles.get("nav_button", "")
|
||||
select_colours = current_app.jinja_env.globals.get("select_colours", "")
|
||||
parts = []
|
||||
for m in markets:
|
||||
href = market_url(f"/{post_slug}/{m.slug}/")
|
||||
parts.append(sx_call("market-link-nav",
|
||||
href=href, name=m.name, nav_class=nav_class))
|
||||
href=href, name=m.name, nav_class=nav_class,
|
||||
select_colours=select_colours))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
|
||||
_handlers["container-nav"] = _container_nav_handler
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
;; Market admin panel components (page-level admin for markets)
|
||||
|
||||
(defcomp ~market-admin-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
(input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"
|
||||
:placeholder "e.g. Suma, Craft Fair"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
|
||||
|
||||
(defcomp ~market-admin-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
form
|
||||
(div :id "markets-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~market-admin-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
|
||||
|
||||
(defcomp ~market-admin-item (&key href name slug del-url csrf-hdr)
|
||||
(div :class "mt-6 border rounded-lg p-4"
|
||||
(div :class "flex items-center justify-between gap-3"
|
||||
(a :class "flex items-baseline gap-3" :href href
|
||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true"
|
||||
(h3 :class "font-semibold" name)
|
||||
(h4 :class "text-gray-500" (str "/" slug "/")))
|
||||
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
|
||||
:data-confirm true :data-confirm-title "Delete market?"
|
||||
:data-confirm-text "Products will be hidden (soft delete)"
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
@@ -19,9 +19,6 @@
|
||||
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
|
||||
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
|
||||
|
||||
(defcomp ~market-card-label-item (&key label)
|
||||
(li label))
|
||||
|
||||
(defcomp ~market-card-sticker (&key src name ring-cls)
|
||||
(img :src src :alt name :class (str "w-6 h-6" ring-cls)))
|
||||
|
||||
@@ -32,15 +29,9 @@
|
||||
(defcomp ~market-card-highlight (&key pre mid post)
|
||||
(<> pre (mark mid) post))
|
||||
|
||||
(defcomp ~market-card-text (&key text)
|
||||
(<> text))
|
||||
|
||||
;; Price — single component accepts both prices, renders correctly
|
||||
;; Price — delegates to shared ~price
|
||||
(defcomp ~market-card-price (&key special-price regular-price)
|
||||
(div :class "mt-1 flex items-baseline gap-2 justify-center"
|
||||
(when special-price (div :class "text-lg font-semibold text-emerald-700" special-price))
|
||||
(when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price))
|
||||
(when (and (not special-price) regular-price) (div :class "mt-1 text-lg font-semibold" regular-price))))
|
||||
(~price :special-price special-price :regular-price regular-price))
|
||||
|
||||
;; Main product card — accepts pure data, composes sub-components
|
||||
(defcomp ~market-product-card (&key href hx-select
|
||||
@@ -104,31 +95,3 @@
|
||||
(if desc-content desc-content (when desc desc)))
|
||||
(if badge-content badge-content (when badge badge))))
|
||||
|
||||
(defcomp ~market-sentinel-mobile (&key id next-url hyperscript)
|
||||
(div :id id
|
||||
:class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry"
|
||||
:sx-swap "outerHTML"
|
||||
:_ hyperscript
|
||||
:role "status" :aria-live "polite" :aria-hidden "true"
|
||||
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
|
||||
(div :class "js-neterr hidden text-center text-xs text-stone-400" "Retrying...")))
|
||||
|
||||
(defcomp ~market-sentinel-desktop (&key id next-url hyperscript)
|
||||
(div :id id
|
||||
:class "hidden md:block h-4 opacity-0 pointer-events-none"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry"
|
||||
:sx-swap "outerHTML"
|
||||
:_ hyperscript
|
||||
:role "status" :aria-live "polite" :aria-hidden "true"
|
||||
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
|
||||
(div :class "js-neterr hidden text-center text-xs text-stone-400" "Retrying...")))
|
||||
|
||||
(defcomp ~market-sentinel-end ()
|
||||
(div :class "col-span-full mt-4 text-center text-xs text-stone-400" "End of results"))
|
||||
|
||||
(defcomp ~market-market-sentinel (&key id next-url)
|
||||
(div :id id :class "h-4 opacity-0 pointer-events-none"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms"
|
||||
:sx-swap "outerHTML" :role "status" :aria-hidden "true"
|
||||
(div :class "text-center text-xs text-stone-400" "loading...")))
|
||||
|
||||
@@ -3,17 +3,9 @@
|
||||
(defcomp ~market-markets-grid (&key cards)
|
||||
(div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" cards))
|
||||
|
||||
(defcomp ~market-no-markets (&key message)
|
||||
(div :class "px-3 py-12 text-center text-stone-400"
|
||||
(i :class "fa fa-store text-4xl mb-3" :aria-hidden "true")
|
||||
(p :class "text-lg" message)))
|
||||
|
||||
(defcomp ~market-product-grid (&key cards)
|
||||
(<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" cards) (div :class "pb-8")))
|
||||
|
||||
(defcomp ~market-bottom-spacer ()
|
||||
(div :class "pb-8"))
|
||||
|
||||
(defcomp ~market-like-toggle-button (&key colour action hx-headers label icon-cls)
|
||||
(button :class (str "flex items-center gap-1 " colour " hover:text-red-600 transition-colors w-[1em] h-[1em]")
|
||||
:sx-post action :sx-target "this" :sx-swap "outerHTML" :sx-push-url "false"
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
(div :class "flex flex-col md:flex-row md:gap-2 text-xs"
|
||||
(div top-slug) sub-div)))
|
||||
|
||||
(defcomp ~market-sub-slug (&key sub)
|
||||
(div sub))
|
||||
|
||||
(defcomp ~market-product-label (&key title)
|
||||
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title)))
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
import os
|
||||
from typing import Any
|
||||
from shared.sx.jinja_bridge import load_service_components
|
||||
from shared.sx.parser import serialize
|
||||
from shared.sx.helpers import (
|
||||
call_url, get_asset_url, sx_call, SxExpr,
|
||||
root_header_sx,
|
||||
@@ -102,7 +103,7 @@ def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
sub_slug = ctx.get("sub_slug", "")
|
||||
hx_select_search = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
sub_div = sx_call("market-sub-slug", sub=sub_slug) if sub_slug else ""
|
||||
sub_div = f'(div {serialize(sub_slug)})' if sub_slug else ""
|
||||
label_sx = sx_call(
|
||||
"market-shop-label",
|
||||
title=market_title, top_slug=top_slug or "",
|
||||
@@ -439,14 +440,14 @@ def _product_cards_sx(ctx: dict) -> str:
|
||||
else:
|
||||
next_qs = f"?page={page + 1}"
|
||||
next_url = prefix + current_local_href + next_qs
|
||||
parts.append(sx_call("market-sentinel-mobile",
|
||||
parts.append(sx_call("sentinel-mobile",
|
||||
id=f"sentinel-{page}-m", next_url=next_url,
|
||||
hyperscript=_MOBILE_SENTINEL_HS))
|
||||
parts.append(sx_call("market-sentinel-desktop",
|
||||
parts.append(sx_call("sentinel-desktop",
|
||||
id=f"sentinel-{page}-d", next_url=next_url,
|
||||
hyperscript=_DESKTOP_SENTINEL_HS))
|
||||
else:
|
||||
parts.append(sx_call("market-sentinel-end"))
|
||||
parts.append(sx_call("end-of-results"))
|
||||
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
|
||||
@@ -1170,7 +1171,7 @@ def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool,
|
||||
post_slug=post_slug) for m in markets]
|
||||
if has_more:
|
||||
parts.append(sx_call(
|
||||
"market-market-sentinel",
|
||||
"sentinel-simple",
|
||||
id=f"sentinel-{page}", next_url=next_url,
|
||||
))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
@@ -1197,7 +1198,8 @@ def _markets_grid(cards_sx: str) -> str:
|
||||
|
||||
def _no_markets_sx(message: str = "No markets available") -> str:
|
||||
"""Empty state for markets as sx."""
|
||||
return sx_call("market-no-markets", message=message)
|
||||
return sx_call("empty-state", icon="fa fa-store", message=message,
|
||||
cls="px-3 py-12 text-center text-stone-400")
|
||||
|
||||
|
||||
async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
|
||||
@@ -1214,7 +1216,7 @@ async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
|
||||
content = _markets_grid(cards)
|
||||
else:
|
||||
content = _no_markets_sx()
|
||||
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
|
||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||
|
||||
hdr = root_header_sx(ctx)
|
||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
||||
@@ -1234,7 +1236,7 @@ async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool,
|
||||
content = _markets_grid(cards)
|
||||
else:
|
||||
content = _no_markets_sx()
|
||||
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
|
||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||
|
||||
oobs = root_header_sx(ctx, oob=True)
|
||||
return oob_page_sx(oobs=oobs, content=content)
|
||||
@@ -1272,7 +1274,7 @@ async def render_page_markets_page(ctx: dict, markets: list, has_more: bool,
|
||||
content = _markets_grid(cards)
|
||||
else:
|
||||
content = _no_markets_sx("No markets for this page")
|
||||
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
|
||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||
|
||||
hdr = root_header_sx(ctx)
|
||||
hdr = "(<> " + hdr + " " + header_child_sx(_post_header_sx(ctx)) + ")"
|
||||
@@ -1296,7 +1298,7 @@ async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool,
|
||||
content = _markets_grid(cards)
|
||||
else:
|
||||
content = _no_markets_sx("No markets for this page")
|
||||
content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")"
|
||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||
|
||||
oobs = _oob_header_sx("post-header-child", "market-header-child", "")
|
||||
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + ")"
|
||||
@@ -1535,12 +1537,17 @@ async def _markets_admin_panel_sx(ctx: dict) -> str:
|
||||
form_html = ""
|
||||
if can_create:
|
||||
create_url = url_for("page_admin.create_market")
|
||||
form_html = sx_call("market-admin-create-form",
|
||||
create_url=create_url, csrf=csrf)
|
||||
form_html = sx_call("crud-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
errors_id="market-create-errors",
|
||||
list_id="markets-list",
|
||||
placeholder="e.g. Suma, Craft Fair",
|
||||
btn_label="Add market")
|
||||
|
||||
list_html = _markets_admin_list_sx(ctx, markets)
|
||||
return sx_call("market-admin-panel",
|
||||
form=SxExpr(form_html), list=SxExpr(list_html))
|
||||
return sx_call("crud-panel",
|
||||
form=SxExpr(form_html), list=SxExpr(list_html),
|
||||
list_id="markets-list")
|
||||
|
||||
|
||||
def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
|
||||
@@ -1552,7 +1559,9 @@ def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
|
||||
prefix = route_prefix()
|
||||
|
||||
if not markets:
|
||||
return sx_call("market-admin-empty")
|
||||
return sx_call("empty-state",
|
||||
message="No markets yet. Create one above.",
|
||||
cls="text-gray-500 mt-4")
|
||||
|
||||
parts = []
|
||||
for m in markets:
|
||||
@@ -1562,9 +1571,12 @@ def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
|
||||
href = prefix + f"/{post_slug}/{m_slug}/"
|
||||
del_url = url_for("page_admin.delete_market", market_slug=m_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
parts.append(sx_call("market-admin-item",
|
||||
parts.append(sx_call("crud-item",
|
||||
href=href, name=m_name, slug=m_slug,
|
||||
del_url=del_url, csrf_hdr=csrf_hdr))
|
||||
del_url=del_url, csrf_hdr=csrf_hdr,
|
||||
list_id="markets-list",
|
||||
confirm_title="Delete market?",
|
||||
confirm_text="Products will be hidden (soft delete)"))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user