Move market composition from Python to .sx defcomps (Phase 3)

Python sxc/pages/ functions no longer build nested sx_call chains or
reference leaf component names. Instead they extract data (URLs, prices,
CSRF, cart state) and call a single top-level composition defcomp with
pure data values. The .sx defcomps handle all component-to-component
wiring, iteration (map), and conditional rendering.

New .sx composition defcomps:
- headers.sx: ~market-header-from-data, ~market-desktop-nav-from-data,
  ~market-product-header-from-data, ~market-product-admin-header-from-data
- prices.sx: ~market-prices-header-from-data, ~market-card-price-from-data
- navigation.sx: ~market-mobile-nav-from-data
- cards.sx: ~market-product-cards-content, ~market-card-from-data,
  ~market-cards-content, ~market-landing-from-data
- detail.sx: ~market-product-detail-from-data, ~market-detail-gallery-from-data,
  ~market-detail-info-from-data
- meta.sx: ~market-product-meta-from-data
- filters.sx: ~market-desktop-filter-from-data, ~market-mobile-chips-from-data,
  ~market-mobile-filter-content-from-data, plus 6 sub-composition defcomps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 01:11:57 +00:00
parent 36a0bd8577
commit e81d77437e
12 changed files with 961 additions and 781 deletions

View File

@@ -95,3 +95,63 @@
(if desc-content desc-content (when desc desc))) (if desc-content desc-content (when desc desc)))
(if badge-content badge-content (when badge badge)))) (if badge-content badge-content (when badge badge))))
;; ---------------------------------------------------------------------------
;; Composition defcomps — receive data lists, compose component trees
;; ---------------------------------------------------------------------------
;; Product cards grid with infinite scroll sentinels
(defcomp ~market-product-cards-content (&key products page total-pages next-url
mobile-sentinel-hs desktop-sentinel-hs)
(<>
(map (lambda (p)
(~market-product-card
:href (get p "href") :hx-select (get p "hx-select") :slug (get p "slug")
:image (get p "image") :brand (get p "brand") :brand-highlight (get p "brand-highlight")
:special-price (get p "special-price") :regular-price (get p "regular-price")
:cart-action (get p "cart-action") :quantity (get p "quantity")
:cart-href (get p "cart-href") :csrf (get p "csrf")
:title (get p "title") :has-like (get p "has-like")
:liked (get p "liked") :like-action (get p "like-action")
:labels (get p "labels") :stickers (get p "stickers")
:has-highlight (get p "has-highlight")
:search-pre (get p "search-pre") :search-mid (get p "search-mid")
:search-post (get p "search-post")))
products)
(if (< page total-pages)
(<> (~sentinel-mobile :id (str "sentinel-" page "-m") :next-url next-url
:hyperscript mobile-sentinel-hs)
(~sentinel-desktop :id (str "sentinel-" page "-d") :next-url next-url
:hyperscript desktop-sentinel-hs))
(~end-of-results))))
;; Single market card from data (handles conditional title/desc/badge)
(defcomp ~market-card-from-data (&key name description href show-badge badge-href badge-title)
(~market-market-card
:title-content (if href
(~market-market-card-title-link :href href :name name)
(~market-market-card-title :name name))
:desc-content (when description
(~market-market-card-desc :description description))
:badge-content (when (and show-badge badge-title)
(~market-market-card-badge :href badge-href :title badge-title))))
;; Market cards list with infinite scroll sentinel
(defcomp ~market-cards-content (&key markets page has-more next-url)
(<>
(map (lambda (m)
(~market-card-from-data
:name (get m "name") :description (get m "description")
:href (get m "href") :show-badge (get m "show-badge")
:badge-href (get m "badge-href") :badge-title (get m "badge-title")))
markets)
(when has-more
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
;; Market landing page content from data
(defcomp ~market-landing-from-data (&key excerpt feature-image html)
(~market-landing-content :inner
(<> (when excerpt (~market-landing-excerpt :text excerpt))
(when feature-image (~market-landing-image :src feature-image))
(when html (~market-landing-html :html html)))))

View File

@@ -92,3 +92,71 @@
(defcomp ~market-landing-content (&key inner) (defcomp ~market-landing-content (&key inner)
(<> (article :class "relative w-full" inner) (div :class "pb-8"))) (<> (article :class "relative w-full" inner) (div :class "pb-8")))
;; ---------------------------------------------------------------------------
;; Composition: product detail page from data
;; ---------------------------------------------------------------------------
;; Gallery section from pre-computed data
(defcomp ~market-detail-gallery-from-data (&key images labels brand like-data has-nav-buttons thumbs)
(let ((like-sx (when like-data
(~market-like-button
:form-id (get like-data "form-id") :action (get like-data "action")
:slug (get like-data "slug") :csrf (get like-data "csrf")
:icon-cls (get like-data "icon-cls")))))
(if images
(<>
(~market-detail-gallery
:inner (~market-detail-gallery-inner
:like like-sx
:image (get (first images) "src") :alt (get (first images) "alt")
:labels (when labels
(<> (map (lambda (src) (~market-label-overlay :src src)) labels)))
:brand brand)
:nav (when has-nav-buttons (~market-detail-nav-buttons)))
(when thumbs
(~market-detail-thumbs :thumbs
(<> (map (lambda (t)
(~market-detail-thumb
:title (get t "title") :src (get t "src") :alt (get t "alt")))
thumbs)))))
(~market-detail-no-image :like like-sx))))
;; Right column details from data
(defcomp ~market-detail-info-from-data (&key extras desc-short desc-html sections)
(~market-detail-right-col :inner
(<>
(when extras
(~market-detail-extras :inner
(<> (map (lambda (e)
(if (= (get e "type") "unit-price")
(~market-detail-unit-price :price (get e "value"))
(~market-detail-case-size :size (get e "value"))))
extras))))
(when (or desc-short desc-html)
(~market-detail-desc-wrapper :inner
(<> (when desc-short (~market-detail-desc-short :text desc-short))
(when desc-html (~market-detail-desc-html :html desc-html)))))
(when sections
(~market-detail-sections :items
(<> (map (lambda (s)
(~market-detail-section :title (get s "title") :html (get s "html")))
sections)))))))
;; Full product detail layout from data
(defcomp ~market-product-detail-from-data (&key images labels brand like-data
has-nav-buttons thumbs sticker-items
extras desc-short desc-html sections)
(~market-detail-layout
:gallery (~market-detail-gallery-from-data
:images images :labels labels :brand brand :like-data like-data
:has-nav-buttons has-nav-buttons :thumbs thumbs)
:stickers (when sticker-items
(~market-detail-stickers :items
(<> (map (lambda (s)
(~market-detail-sticker :src (get s "src") :name (get s "name")))
sticker-items))))
:details (~market-detail-info-from-data
:extras extras :desc-short desc-short :desc-html desc-html
:sections sections)))

View File

@@ -122,3 +122,152 @@
(defcomp ~market-mobile-chip-brand-list (&key items) (defcomp ~market-mobile-chip-brand-list (&key items)
(ul items)) (ul items))
;; ---------------------------------------------------------------------------
;; Composition defcomps — receive data, compose filter component trees
;; ---------------------------------------------------------------------------
;; Sort option stickers from data
(defcomp ~market-filter-sort-from-data (&key items)
(~market-filter-sort-row :items
(<> (map (lambda (s)
(~market-filter-sort-item
:href (get s "href") :hx-select (get s "hx-select")
:ring-cls (get s "ring-cls") :src (get s "src") :label (get s "label")))
items))))
;; Like filter from data
(defcomp ~market-filter-like-from-data (&key href hx-select liked mobile)
(~market-filter-like
:href href :hx-select hx-select
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")
:size-cls (if mobile "text-[40px]" "text-2xl")))
;; Label filter items from data
(defcomp ~market-filter-labels-from-data (&key items hx-select)
(<> (map (lambda (lb)
(~market-filter-label-item
:href (get lb "href") :hx-select hx-select
:ring-cls (get lb "ring-cls") :src (get lb "src") :name (get lb "name")))
items)))
;; Sticker filter items from data
(defcomp ~market-filter-stickers-from-data (&key items hx-select)
(~market-filter-stickers-row :items
(<> (map (lambda (st)
(~market-filter-sticker-item
:href (get st "href") :hx-select hx-select
:ring-cls (get st "ring-cls") :src (get st "src") :name (get st "name")
:count-cls (get st "count-cls") :count (get st "count")))
items))))
;; Brand filter items from data
(defcomp ~market-filter-brands-from-data (&key items hx-select)
(~market-filter-brands-panel :items
(<> (map (lambda (br)
(~market-filter-brand-item
:href (get br "href") :hx-select hx-select
:bg-cls (get br "bg-cls") :name-cls (get br "name-cls")
:name (get br "name") :count (get br "count")))
items))))
;; Subcategory selector from data
(defcomp ~market-filter-subcategories-from-data (&key items hx-select all-href current-sub)
(~market-filter-subcategory-panel :items
(<>
(~market-filter-subcategory-item
:href all-href :hx-select hx-select
:active-cls (if (not current-sub) " bg-stone-200 font-medium" "")
:name "All")
(map (lambda (sub)
(~market-filter-subcategory-item
:href (get sub "href") :hx-select hx-select
:active-cls (get sub "active-cls") :name (get sub "name")))
items))))
;; Desktop filter panel from data
(defcomp ~market-desktop-filter-from-data (&key search-sx category-label
sort-data like-data label-data
sticker-data brand-data sub-data hx-select)
(<>
search-sx
(~market-desktop-category-summary :inner
(<>
(~market-filter-category-label :label category-label)
(when sort-data (~market-filter-sort-from-data :items sort-data))
(~market-filter-like-labels-nav :inner
(<>
(~market-filter-like-from-data
:href (get like-data "href") :hx-select hx-select
:liked (get like-data "liked") :mobile false)
(when label-data
(~market-filter-labels-from-data :items label-data :hx-select hx-select))))
(when sticker-data
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select))
(when sub-data
(~market-filter-subcategories-from-data
:items (get sub-data "items") :hx-select hx-select
:all-href (get sub-data "all-href")
:current-sub (get sub-data "current-sub")))))
(~market-desktop-brand-summary
:inner (when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select)))))
;; Mobile filter chips from active filter data
(defcomp ~market-mobile-chips-from-data (&key sort-chip liked-chip label-chips sticker-chips brand-chips)
(~market-mobile-chips-row :inner
(<>
(when sort-chip
(~market-mobile-chip-sort :src (get sort-chip "src") :label (get sort-chip "label")))
(when liked-chip
(~market-mobile-chip-liked :inner
(<>
(~market-mobile-chip-liked-icon)
(when (get liked-chip "count")
(~market-mobile-chip-count
:cls (get liked-chip "count-cls") :count (get liked-chip "count"))))))
(when label-chips
(~market-mobile-chip-list :items
(<> (map (lambda (lc)
(~market-mobile-chip-item :inner
(<>
(~market-mobile-chip-image :src (get lc "src") :name (get lc "name"))
(when (get lc "count")
(~market-mobile-chip-count :cls (get lc "count-cls") :count (get lc "count"))))))
label-chips))))
(when sticker-chips
(~market-mobile-chip-list :items
(<> (map (lambda (sc)
(~market-mobile-chip-item :inner
(<>
(~market-mobile-chip-image :src (get sc "src") :name (get sc "name"))
(when (get sc "count")
(~market-mobile-chip-count :cls (get sc "count-cls") :count (get sc "count"))))))
sticker-chips))))
(when brand-chips
(~market-mobile-chip-brand-list :items
(<> (map (lambda (bc)
(if (get bc "has-count")
(~market-mobile-chip-brand :name (get bc "name") :count (get bc "count"))
(~market-mobile-chip-brand-zero :name (get bc "name"))))
brand-chips)))))))
;; Mobile filter content (expanded panel) from data
(defcomp ~market-mobile-filter-content-from-data (&key sort-data like-data label-data
sticker-data brand-data clear-href hx-select)
(<>
(when sort-data (~market-filter-sort-from-data :items sort-data))
(when clear-href
(~market-mobile-clear-filters :href clear-href :hx-select hx-select))
(~market-mobile-like-labels-row :inner
(<>
(~market-filter-like-from-data
:href (get like-data "href") :hx-select hx-select
:liked (get like-data "liked") :mobile true)
(when label-data
(~market-filter-labels-from-data :items label-data :hx-select hx-select))))
(when sticker-data
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select))
(when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select))))

View File

@@ -15,3 +15,64 @@
:class "px-2 py-1 text-stone-500 hover:text-stone-700" :class "px-2 py-1 text-stone-500 hover:text-stone-700"
(i :class "fa fa-cog" :aria-hidden "true"))) (i :class "fa fa-cog" :aria-hidden "true")))
;; ---------------------------------------------------------------------------
;; Composition defcomps — receive data, compose component trees
;; ---------------------------------------------------------------------------
;; Desktop category nav from pre-computed category data
(defcomp ~market-desktop-nav-from-data (&key categories hx-select select-colours
all-href all-active admin-href)
(~market-desktop-category-nav
:links (<>
(~market-category-link :href all-href :hx-select hx-select
:active all-active :select-colours select-colours :label "All")
(map (lambda (cat)
(~market-category-link
:href (get cat "href") :hx-select hx-select
:active (get cat "active") :select-colours select-colours
:label (get cat "label"))) categories))
:admin (when admin-href
(~market-admin-link :href admin-href :hx-select hx-select))))
;; Market-level header row from data
(defcomp ~market-header-from-data (&key market-title top-slug sub-slug link-href
categories hx-select select-colours
all-href all-active admin-href oob)
(~menu-row-sx :id "market-row" :level 2
:link-href link-href
:link-label-content (~market-shop-label
:title market-title :top-slug (or top-slug "") :sub-div sub-slug)
:nav (~market-desktop-nav-from-data
:categories categories :hx-select hx-select :select-colours select-colours
:all-href all-href :all-active all-active :admin-href admin-href)
:child-id "market-header-child"
:oob oob))
;; Product-level header row from data
(defcomp ~market-product-header-from-data (&key title link-href hx-select
price-data admin-href oob)
(~menu-row-sx :id "product-row" :level 3
:link-href link-href
:link-label-content (~market-product-label :title title)
:nav (<>
(~market-prices-header-from-data
:cart-id (get price-data "cart-id")
:cart-action (get price-data "cart-action")
:csrf (get price-data "csrf")
:quantity (get price-data "quantity")
:cart-href (get price-data "cart-href")
:sp-val (get price-data "sp-val") :sp-str (get price-data "sp-str")
:rp-val (get price-data "rp-val") :rp-str (get price-data "rp-str")
:rrp-str (get price-data "rrp-str"))
(when admin-href
(~market-admin-link :href admin-href :hx-select hx-select)))
:child-id "product-header-child"
:oob oob))
;; Product admin header row from data
(defcomp ~market-product-admin-header-from-data (&key link-href oob)
(~menu-row-sx :id "product-admin-row" :level 4
:link-href link-href :link-label "admin!!" :icon "fa fa-cog"
:child-id "product-admin-header-child" :oob oob))

View File

@@ -17,3 +17,35 @@
(defcomp ~market-meta-jsonld (&key json) (defcomp ~market-meta-jsonld (&key json)
(script :type "application/ld+json" (~rich-text :html json))) (script :type "application/ld+json" (~rich-text :html json)))
;; ---------------------------------------------------------------------------
;; Composition: all product meta tags from data
;; ---------------------------------------------------------------------------
(defcomp ~market-product-meta-from-data (&key title description canonical image-url
site-title brand price price-currency
jsonld-json)
(<>
(~market-meta-title :title title)
(~market-meta-description :description description)
(when canonical (~market-meta-canonical :href canonical))
;; OpenGraph
(~market-meta-og :property "og:site_name" :content site-title)
(~market-meta-og :property "og:type" :content "product")
(~market-meta-og :property "og:title" :content title)
(~market-meta-og :property "og:description" :content description)
(when canonical (~market-meta-og :property "og:url" :content canonical))
(when image-url (~market-meta-og :property "og:image" :content image-url))
(when (and price price-currency)
(<> (~market-meta-og :property "product:price:amount" :content price)
(~market-meta-og :property "product:price:currency" :content price-currency)))
(when brand (~market-meta-og :property "product:brand" :content brand))
;; Twitter
(~market-meta-twitter :name "twitter:card"
:content (if image-url "summary_large_image" "summary"))
(~market-meta-twitter :name "twitter:title" :content title)
(~market-meta-twitter :name "twitter:description" :content description)
(when image-url (~market-meta-twitter :name "twitter:image" :content image-url))
;; JSON-LD
(~market-meta-jsonld :json jsonld-json)))

View File

@@ -61,3 +61,37 @@
(defcomp ~market-mobile-cat-details (&key open summary subs) (defcomp ~market-mobile-cat-details (&key open summary subs)
(details :class "group/cat py-1" :open open (details :class "group/cat py-1" :open open
summary subs)) summary subs))
;; ---------------------------------------------------------------------------
;; Composition: mobile nav panel from pre-computed category data
;; ---------------------------------------------------------------------------
(defcomp ~market-mobile-nav-from-data (&key categories all-href all-active hx-select select-colours)
(~market-mobile-nav-wrapper :items
(<>
(~market-mobile-all-link :href all-href :hx-select hx-select
:active all-active :select-colours select-colours)
(map (lambda (cat)
(~market-mobile-cat-details
:open (get cat "active")
:summary (~market-mobile-cat-summary
:bg-cls (if (get cat "active") " bg-stone-900 text-white hover:bg-stone-900" "")
:href (get cat "href") :hx-select hx-select
:select-colours select-colours :cat-name (get cat "name")
:count-label (str (get cat "count") " products")
:count-str (str (get cat "count"))
:chevron (~market-mobile-chevron))
:subs (if (get cat "subs")
(~market-mobile-subs-panel :links
(<> (map (lambda (sub)
(~market-mobile-sub-link
:select-colours select-colours
:active (get sub "active")
:href (get sub "href") :hx-select hx-select
:label (get sub "label")
:count-label (str (get sub "count") " products")
:count-str (str (get sub "count"))))
(get cat "subs"))))
(~market-mobile-view-all :href (get cat "href") :hx-select hx-select))))
categories))))

View File

@@ -32,3 +32,36 @@
(defcomp ~market-prices-row (&key inner) (defcomp ~market-prices-row (&key inner)
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner)) (div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner))
;; ---------------------------------------------------------------------------
;; Composition: prices header + cart button from data
;; ---------------------------------------------------------------------------
(defcomp ~market-prices-header-from-data (&key cart-id cart-action csrf quantity cart-href
sp-val sp-str rp-val rp-str rrp-str)
(~market-prices-row :inner
(<>
(if quantity
(~market-cart-add-quantity :cart-id cart-id :action cart-action :csrf csrf
:minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1))
:quantity (str quantity) :cart-href cart-href)
(~market-cart-add-empty :cart-id cart-id :action cart-action :csrf csrf))
(when sp-val
(<> (~market-header-price-special-label)
(~market-header-price-special :price sp-str)
(when rp-val (~market-header-price-strike :price rp-str))))
(when (and (not sp-val) rp-val)
(<> (~market-header-price-regular-label)
(~market-header-price-regular :price rp-str)))
(when rrp-str (~market-header-rrp :rrp rrp-str)))))
;; Card price line from data (used in product cards)
(defcomp ~market-card-price-from-data (&key sp-val sp-str rp-val rp-str)
(~market-price-line :inner
(<>
(when sp-val
(<> (~market-price-special :price sp-str)
(when rp-val (~market-price-regular-strike :price rp-str))))
(when (and (not sp-val) rp-val)
(~market-price-regular :price rp-str)))))

View File

@@ -1,9 +1,8 @@
"""Product/market card builders.""" """Product/market card data builders."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import sx_call from shared.sx.helpers import sx_call
from .utils import _set_prices, _price_str from .utils import _set_prices, _price_str
@@ -11,11 +10,11 @@ from .filters import _MOBILE_SENTINEL_HS, _DESKTOP_SENTINEL_HS
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Product card (browse grid item) # Product card data extraction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _product_card_sx(p: dict, ctx: dict) -> str: def _product_card_data(p: dict, ctx: dict) -> dict:
"""Build a single product card for browse grid as sx call.""" """Extract data for a single product card."""
from quart import url_for from quart import url_for
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
from shared.utils import route_prefix from shared.utils import route_prefix
@@ -49,7 +48,8 @@ def _product_card_sx(p: dict, ctx: dict) -> str:
if raw_stickers and callable(asset_url_fn): if raw_stickers and callable(asset_url_fn):
for s in raw_stickers: for s in raw_stickers:
ring = " ring-2 ring-emerald-500 rounded" if s in selected_stickers else "" ring = " ring-2 ring-emerald-500 rounded" if s in selected_stickers else ""
sticker_data.append({"src": asset_url_fn(f"stickers/{s}.svg"), "name": s, "ring-cls": ring}) sticker_data.append({"src": asset_url_fn(f"stickers/{s}.svg"),
"name": s, "ring-cls": ring})
# Title highlighting # Title highlighting
title = p.get("title", "") title = p.get("title", "")
@@ -71,38 +71,37 @@ def _product_card_sx(p: dict, ctx: dict) -> str:
brand = p.get("brand", "") brand = p.get("brand", "")
brand_highlight = " bg-yellow-200" if brand in selected_brands else "" brand_highlight = " bg-yellow-200" if brand in selected_brands else ""
kwargs = dict( d: dict[str, Any] = {
href=item_href, hx_select=hx_select, slug=slug, "href": item_href, "hx-select": hx_select, "slug": slug,
image=p.get("image", ""), brand=brand, brand_highlight=brand_highlight, "image": p.get("image", ""), "brand": brand, "brand-highlight": brand_highlight,
special_price=sp_str, regular_price=rp_str, "special-price": sp_str, "regular-price": rp_str,
cart_action=cart_action, quantity=quantity, cart_href=cart_href, csrf=csrf, "cart-action": cart_action, "quantity": quantity, "cart-href": cart_href, "csrf": csrf,
title=title, "title": title, "has-like": bool(user),
has_like=bool(user), }
)
if label_srcs: if label_srcs:
kwargs["labels"] = label_srcs d["labels"] = label_srcs
elif labels: elif labels:
kwargs["labels"] = labels d["labels"] = labels
if user: if user:
kwargs["liked"] = p.get("is_liked", False) d["liked"] = p.get("is_liked", False)
kwargs["like_action"] = url_for("market.browse.product.like_toggle", product_slug=slug) d["like-action"] = url_for("market.browse.product.like_toggle", product_slug=slug)
if sticker_data: if sticker_data:
kwargs["stickers"] = sticker_data d["stickers"] = sticker_data
if has_highlight: if has_highlight:
kwargs["has_highlight"] = True d["has-highlight"] = True
kwargs["search_pre"] = search_pre d["search-pre"] = search_pre
kwargs["search_mid"] = search_mid d["search-mid"] = search_mid
kwargs["search_post"] = search_post d["search-post"] = search_post
return sx_call("market-product-card", **kwargs) return d
def _product_cards_sx(ctx: dict) -> str: def _product_cards_sx(ctx: dict) -> str:
"""S-expression wire format for product cards (client renders).""" """S-expression wire format for product cards — delegates to .sx defcomp."""
from shared.utils import route_prefix from shared.utils import route_prefix
prefix = route_prefix() prefix = route_prefix()
@@ -112,48 +111,46 @@ def _product_cards_sx(ctx: dict) -> str:
current_local_href = ctx.get("current_local_href", "/") current_local_href = ctx.get("current_local_href", "/")
qs_fn = ctx.get("qs_filter") qs_fn = ctx.get("qs_filter")
parts = [] product_data = [_product_card_data(p, ctx) for p in products]
for p in products:
parts.append(_product_card_sx(p, ctx))
next_url = ""
if page < total_pages: if page < total_pages:
if callable(qs_fn): if callable(qs_fn):
next_qs = qs_fn({"page": page + 1}) next_qs = qs_fn({"page": page + 1})
else: else:
next_qs = f"?page={page + 1}" next_qs = f"?page={page + 1}"
next_url = prefix + current_local_href + next_qs next_url = prefix + current_local_href + next_qs
parts.append(sx_call("sentinel-mobile",
id=f"sentinel-{page}-m", next_url=next_url,
hyperscript=_MOBILE_SENTINEL_HS))
parts.append(sx_call("sentinel-desktop",
id=f"sentinel-{page}-d", next_url=next_url,
hyperscript=_DESKTOP_SENTINEL_HS))
else:
parts.append(sx_call("end-of-results"))
return "(<> " + " ".join(parts) + ")" return sx_call("market-product-cards-content",
products=product_data,
page=page,
total_pages=total_pages,
next_url=next_url,
mobile_sentinel_hs=_MOBILE_SENTINEL_HS,
desktop_sentinel_hs=_DESKTOP_SENTINEL_HS)
def _like_button_sx(slug: str, liked: bool, csrf: str, ctx: dict) -> str: def _like_button_data(slug: str, liked: bool, csrf: str, ctx: dict) -> dict:
"""Build the like/unlike heart button overlay as sx.""" """Extract like button data."""
from quart import url_for from quart import url_for
action = url_for("market.browse.product.like_toggle", product_slug=slug) action = url_for("market.browse.product.like_toggle", product_slug=slug)
icon_cls = "fa-solid fa-heart text-red-500" if liked else "fa-regular fa-heart text-stone-400" icon_cls = "fa-solid fa-heart text-red-500" if liked else "fa-regular fa-heart text-stone-400"
return sx_call( return {
"market-like-button", "form-id": f"like-{slug}",
form_id=f"like-{slug}", action=action, slug=slug, "action": action,
csrf=csrf, icon_cls=icon_cls, "slug": slug,
) "csrf": csrf,
"icon-cls": icon_cls,
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Market cards (all markets / page markets) # Market cards data extraction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _market_card_sx(market: Any, page_info: dict, *, show_page_badge: bool = True, def _market_card_data(market: Any, page_info: dict, *, show_page_badge: bool = True,
post_slug: str = "") -> str: post_slug: str = "") -> dict:
"""Build a single market card as sx.""" """Extract data for a single market card."""
from shared.infrastructure.urls import market_url from shared.infrastructure.urls import market_url
name = getattr(market, "name", "") name = getattr(market, "name", "")
@@ -161,78 +158,60 @@ def _market_card_sx(market: Any, page_info: dict, *, show_page_badge: bool = Tru
slug = getattr(market, "slug", "") slug = getattr(market, "slug", "")
container_id = getattr(market, "container_id", None) container_id = getattr(market, "container_id", None)
href = ""
badge_href = ""
badge_title = ""
if show_page_badge and page_info: if show_page_badge and page_info:
pi = page_info.get(container_id, {}) pi = page_info.get(container_id, {})
p_slug = pi.get("slug", "") p_slug = pi.get("slug", "")
p_title = pi.get("title", "") p_title = pi.get("title", "")
market_href = market_url(f"/{p_slug}/{slug}/") if p_slug else "" href = market_url(f"/{p_slug}/{slug}/") if p_slug else ""
if p_title:
badge_href = market_url(f"/{p_slug}/")
badge_title = p_title
else: else:
p_slug = post_slug p_slug = post_slug
p_title = "" href = market_url(f"/{post_slug}/{slug}/") if post_slug else ""
market_href = market_url(f"/{post_slug}/{slug}/") if post_slug else ""
title_sx = "" return {
if market_href: "name": name,
title_sx = sx_call("market-market-card-title-link", href=market_href, name=name) "description": description,
else: "href": href,
title_sx = sx_call("market-market-card-title", name=name) "show-badge": show_page_badge,
"badge-href": badge_href,
desc_sx = "" "badge-title": badge_title,
if description: }
desc_sx = sx_call("market-market-card-desc", description=description)
badge_sx = ""
if show_page_badge and p_title:
badge_href = market_url(f"/{p_slug}/")
badge_sx = sx_call("market-market-card-badge", href=badge_href, title=p_title)
return sx_call(
"market-market-card",
title_content=title_sx or None,
desc_content=desc_sx or None,
badge_content=badge_sx or None,
)
def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool, def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool,
next_url: str, *, show_page_badge: bool = True, next_url: str, *, show_page_badge: bool = True,
post_slug: str = "") -> str: post_slug: str = "") -> str:
"""Build market cards with infinite scroll sentinel as sx.""" """Build market cards as sx — delegates to .sx defcomp."""
parts = [] market_data = [_market_card_data(m, page_info, show_page_badge=show_page_badge,
for m in markets: post_slug=post_slug) for m in markets]
parts.append(_market_card_sx(m, page_info, show_page_badge=show_page_badge, return sx_call("market-cards-content",
post_slug=post_slug)) markets=market_data,
if has_more: page=page,
parts.append(sx_call( has_more=has_more,
"sentinel-simple", next_url=next_url)
id=f"sentinel-{page}", next_url=next_url,
))
return "(<> " + " ".join(parts) + ")"
def _markets_grid(cards_sx: str) -> str: def _markets_grid(cards_sx: str) -> str:
"""Wrap market cards in a grid as sx.""" """Wrap market cards in a grid as sx."""
from shared.sx.parser import SxExpr
return sx_call("market-markets-grid", cards=SxExpr(cards_sx)) return sx_call("market-markets-grid", cards=SxExpr(cards_sx))
def _no_markets_sx(message: str = "No markets available") -> str: def _no_markets_sx(message: str = "No markets available") -> str:
"""Empty state for markets as sx.""" """Empty state for markets as sx."""
return sx_call("empty-state", icon="fa fa-store", message=message, return sx_call("empty-state", icon="fa fa-store", message=message,
cls="px-3 py-12 text-center text-stone-400") cls="px-3 py-12 text-center text-stone-400")
# ---------------------------------------------------------------------------
# Market landing page
# ---------------------------------------------------------------------------
def _market_landing_content_sx(post: dict) -> str: def _market_landing_content_sx(post: dict) -> str:
"""Build market landing page content as sx.""" """Build market landing page content — delegates to .sx defcomp."""
parts: list[str] = [] return sx_call("market-landing-from-data",
if post.get("custom_excerpt"): excerpt=post.get("custom_excerpt") or None,
parts.append(sx_call("market-landing-excerpt", text=post["custom_excerpt"])) feature_image=post.get("feature_image") or None,
if post.get("feature_image"): html=post.get("html") or None)
parts.append(sx_call("market-landing-image", src=post["feature_image"]))
if post.get("html"):
parts.append(sx_call("market-landing-html", html=post["html"]))
inner = "(<> " + " ".join(parts) + ")" if parts else "(<>)"
return sx_call("market-landing-content", inner=SxExpr(inner))

View File

@@ -1,4 +1,4 @@
"""Filter panel functions (mobile + desktop).""" """Filter panel data extraction (mobile + desktop)."""
from __future__ import annotations from __future__ import annotations
from shared.sx.parser import SxExpr from shared.sx.parser import SxExpr
@@ -77,365 +77,308 @@ _DESKTOP_SENTINEL_HS = (
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Browse filter panels (mobile + desktop) # Filter data extraction helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def _desktop_filter_sx(ctx: dict) -> str: def _filter_common(ctx: dict) -> tuple:
"""Build the desktop aside filter panel as sx.""" """Extract common filter params from context."""
category_label = ctx.get("category_label", "")
sort_options = ctx.get("sort_options", [])
sort = ctx.get("sort", "")
labels = ctx.get("labels", [])
selected_labels = ctx.get("selected_labels", [])
stickers = ctx.get("stickers", [])
selected_stickers = ctx.get("selected_stickers", [])
brands = ctx.get("brands", [])
selected_brands = ctx.get("selected_brands", [])
liked = ctx.get("liked", False)
liked_count = ctx.get("liked_count", 0)
subs_local = ctx.get("subs_local", [])
top_local_href = ctx.get("top_local_href", "")
sub_slug = ctx.get("sub_slug", "")
# Search
search_sx = await search_desktop_sx(ctx)
# Category summary + sort + like + labels + stickers
cat_parts = [sx_call("market-filter-category-label", label=category_label)]
if sort_options:
cat_parts.append(_sort_stickers_sx(sort_options, sort, ctx))
like_label_parts = [_like_filter_sx(liked, liked_count, ctx)]
if labels:
like_label_parts.append(_labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels"))
like_labels_sx = "(<> " + " ".join(like_label_parts) + ")"
cat_parts.append(sx_call("market-filter-like-labels-nav", inner=SxExpr(like_labels_sx)))
if stickers:
cat_parts.append(_stickers_filter_sx(stickers, selected_stickers, ctx))
if subs_local and top_local_href:
cat_parts.append(_subcategory_selector_sx(subs_local, top_local_href, sub_slug, ctx))
cat_inner_sx = "(<> " + " ".join(cat_parts) + ")"
cat_summary = sx_call("market-desktop-category-summary", inner=SxExpr(cat_inner_sx))
# Brand filter
brand_inner = ""
if brands:
brand_inner = _brand_filter_sx(brands, selected_brands, ctx)
brand_summary = sx_call("market-desktop-brand-summary",
inner=brand_inner or None)
return "(<> " + " ".join([search_sx, cat_summary, brand_summary]) + ")"
async def _mobile_filter_summary_sx(ctx: dict) -> str:
"""Build mobile filter summary as sx."""
asset_url_fn = ctx.get("asset_url")
sort = ctx.get("sort", "")
sort_options = ctx.get("sort_options", [])
liked = ctx.get("liked", False)
liked_count = ctx.get("liked_count", 0)
selected_labels = ctx.get("selected_labels", [])
selected_stickers = ctx.get("selected_stickers", [])
selected_brands = ctx.get("selected_brands", [])
labels = ctx.get("labels", [])
stickers = ctx.get("stickers", [])
brands = ctx.get("brands", [])
# Search bar
search_bar = await search_mobile_sx(ctx)
# Summary chips showing active filters
chip_parts: list[str] = []
if sort and sort_options:
for k, l, i in sort_options:
if k == sort and callable(asset_url_fn):
chip_parts.append(sx_call("market-mobile-chip-sort", src=asset_url_fn(i), label=l))
if liked:
liked_parts = [sx_call("market-mobile-chip-liked-icon")]
if liked_count is not None:
cls = "text-[10px] text-stone-500" if liked_count != 0 else "text-md text-red-500 font-bold"
liked_parts.append(sx_call("market-mobile-chip-count", cls=cls, count=str(liked_count)))
liked_inner = "(<> " + " ".join(liked_parts) + ")"
chip_parts.append(sx_call("market-mobile-chip-liked", inner=SxExpr(liked_inner)))
# Selected labels
if selected_labels:
label_item_parts = []
for sl in selected_labels:
for lb in labels:
if lb.get("name") == sl and callable(asset_url_fn):
li_parts = [sx_call(
"market-mobile-chip-image",
src=asset_url_fn("nav-labels/" + sl + ".svg"), name=sl,
)]
if lb.get("count") is not None:
cls = "text-[10px] text-stone-500" if lb["count"] != 0 else "text-md text-red-500 font-bold"
li_parts.append(sx_call("market-mobile-chip-count", cls=cls, count=str(lb["count"])))
li_inner = "(<> " + " ".join(li_parts) + ")"
label_item_parts.append(sx_call("market-mobile-chip-item", inner=SxExpr(li_inner)))
if label_item_parts:
label_items = "(<> " + " ".join(label_item_parts) + ")"
chip_parts.append(sx_call("market-mobile-chip-list", items=SxExpr(label_items)))
# Selected stickers
if selected_stickers:
sticker_item_parts = []
for ss in selected_stickers:
for st in stickers:
if st.get("name") == ss and callable(asset_url_fn):
si_parts = [sx_call(
"market-mobile-chip-image",
src=asset_url_fn("stickers/" + ss + ".svg"), name=ss,
)]
if st.get("count") is not None:
cls = "text-[10px] text-stone-500" if st["count"] != 0 else "text-md text-red-500 font-bold"
si_parts.append(sx_call("market-mobile-chip-count", cls=cls, count=str(st["count"])))
si_inner = "(<> " + " ".join(si_parts) + ")"
sticker_item_parts.append(sx_call("market-mobile-chip-item", inner=SxExpr(si_inner)))
if sticker_item_parts:
sticker_items = "(<> " + " ".join(sticker_item_parts) + ")"
chip_parts.append(sx_call("market-mobile-chip-list", items=SxExpr(sticker_items)))
# Selected brands
if selected_brands:
brand_item_parts = []
for b in selected_brands:
count = 0
for br in brands:
if br.get("name") == b:
count = br.get("count", 0)
if count:
brand_item_parts.append(sx_call("market-mobile-chip-brand", name=b, count=str(count)))
else:
brand_item_parts.append(sx_call("market-mobile-chip-brand-zero", name=b))
brand_items = "(<> " + " ".join(brand_item_parts) + ")"
chip_parts.append(sx_call("market-mobile-chip-brand-list", items=SxExpr(brand_items)))
chips_sx = "(<> " + " ".join(chip_parts) + ")" if chip_parts else '(<>)'
chips_row = sx_call("market-mobile-chips-row", inner=SxExpr(chips_sx))
# Full mobile filter details
from shared.utils import route_prefix from shared.utils import route_prefix
prefix = route_prefix() prefix = route_prefix()
mobile_filter = _mobile_filter_content_sx(ctx, prefix)
return sx_call(
"market-mobile-filter-summary",
search_bar=search_bar,
chips=chips_row,
filter=SxExpr(mobile_filter),
)
def _mobile_filter_content_sx(ctx: dict, prefix: str) -> str:
"""Build the expanded mobile filter panel contents as sx."""
selected_labels = ctx.get("selected_labels", [])
selected_stickers = ctx.get("selected_stickers", [])
selected_brands = ctx.get("selected_brands", [])
current_local_href = ctx.get("current_local_href", "/")
hx_select = ctx.get("hx_select_search", "#main-panel")
sort_options = ctx.get("sort_options", [])
sort = ctx.get("sort", "")
liked = ctx.get("liked", False)
liked_count = ctx.get("liked_count", 0)
labels = ctx.get("labels", [])
stickers = ctx.get("stickers", [])
brands = ctx.get("brands", [])
search = ctx.get("search", "")
qs_fn = ctx.get("qs_filter")
parts: list[str] = []
# Sort options
if sort_options:
parts.append(_sort_stickers_sx(sort_options, sort, ctx, mobile=True))
# Clear filters button
has_filters = search or selected_labels or selected_stickers or selected_brands
if has_filters and callable(qs_fn):
clear_url = prefix + current_local_href + qs_fn({"clear_filters": True})
parts.append(sx_call("market-mobile-clear-filters", href=clear_url, hx_select=hx_select))
# Like + labels row
like_label_parts = [_like_filter_sx(liked, liked_count, ctx, mobile=True)]
if labels:
like_label_parts.append(_labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels", mobile=True))
like_labels_sx = "(<> " + " ".join(like_label_parts) + ")"
parts.append(sx_call("market-mobile-like-labels-row", inner=SxExpr(like_labels_sx)))
# Stickers
if stickers:
parts.append(_stickers_filter_sx(stickers, selected_stickers, ctx, mobile=True))
# Brands
if brands:
parts.append(_brand_filter_sx(brands, selected_brands, ctx, mobile=True))
return "(<> " + " ".join(parts) + ")" if parts else "(<>)"
def _sort_stickers_sx(sort_options: list, current_sort: str, ctx: dict, mobile: bool = False) -> str:
"""Build sort option stickers as sx."""
asset_url_fn = ctx.get("asset_url")
current_local_href = ctx.get("current_local_href", "/") current_local_href = ctx.get("current_local_href", "/")
hx_select = ctx.get("hx_select_search", "#main-panel") hx_select = ctx.get("hx_select_search", "#main-panel")
qs_fn = ctx.get("qs_filter") qs_fn = ctx.get("qs_filter")
from shared.utils import route_prefix asset_url_fn = ctx.get("asset_url")
prefix = route_prefix() return prefix, current_local_href, hx_select, qs_fn, asset_url_fn
item_parts: list[str] = []
def _sort_data(sort_options: list, current_sort: str, ctx: dict) -> list:
"""Extract sort option data for .sx composition."""
prefix, current_local_href, hx_select, qs_fn, asset_url_fn = _filter_common(ctx)
items = []
for k, label, icon in sort_options: for k, label, icon in sort_options:
if callable(qs_fn): href = prefix + current_local_href + qs_fn({"sort": k}) if callable(qs_fn) else "#"
href = prefix + current_local_href + qs_fn({"sort": k}) active = k == current_sort
else:
href = "#"
active = (k == current_sort)
ring = " ring-2 ring-emerald-500 rounded" if active else "" ring = " ring-2 ring-emerald-500 rounded" if active else ""
src = asset_url_fn(icon) if callable(asset_url_fn) else icon src = asset_url_fn(icon) if callable(asset_url_fn) else icon
item_parts.append(sx_call( items.append({"href": href, "hx-select": hx_select,
"market-filter-sort-item", "ring-cls": ring, "src": src, "label": label})
href=href, hx_select=hx_select, ring_cls=ring, src=src, label=label, return items
))
items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
return sx_call("market-filter-sort-row", items=SxExpr(items_sx))
def _like_filter_sx(liked: bool, liked_count: int, ctx: dict, mobile: bool = False) -> str: def _like_data(liked: bool, liked_count: int, ctx: dict) -> dict:
"""Build the like filter toggle as sx.""" """Extract like filter data for .sx composition."""
current_local_href = ctx.get("current_local_href", "/") prefix, current_local_href, hx_select, qs_fn, _ = _filter_common(ctx)
hx_select = ctx.get("hx_select_search", "#main-panel") href = prefix + current_local_href + qs_fn({"liked": not liked}) if callable(qs_fn) else "#"
qs_fn = ctx.get("qs_filter") return {"href": href, "hx-select": hx_select, "liked": liked}
from shared.utils import route_prefix
prefix = route_prefix()
if callable(qs_fn):
href = prefix + current_local_href + qs_fn({"liked": not liked})
else:
href = "#"
icon_cls = "fa-solid fa-heart text-red-500" if liked else "fa-regular fa-heart text-stone-400"
size = "text-[40px]" if mobile else "text-2xl"
return sx_call(
"market-filter-like",
href=href, hx_select=hx_select, icon_cls=icon_cls, size_cls=size,
)
def _labels_filter_sx(labels: list, selected: list, ctx: dict, *, def _label_data(labels: list, selected: list, ctx: dict, *, img_prefix: str = "nav-labels") -> list:
prefix: str = "nav-labels", mobile: bool = False) -> str: """Extract label filter data for .sx composition."""
"""Build label filter buttons as sx.""" prefix, current_local_href, hx_select, qs_fn, asset_url_fn = _filter_common(ctx)
asset_url_fn = ctx.get("asset_url") items = []
current_local_href = ctx.get("current_local_href", "/")
hx_select = ctx.get("hx_select_search", "#main-panel")
qs_fn = ctx.get("qs_filter")
from shared.utils import route_prefix
rp = route_prefix()
item_parts: list[str] = []
for lb in labels: for lb in labels:
name = lb.get("name", "") name = lb.get("name", "")
is_sel = name in selected is_sel = name in selected
if callable(qs_fn): if callable(qs_fn):
new_sel = [s for s in selected if s != name] if is_sel else selected + [name] new_sel = [s for s in selected if s != name] if is_sel else selected + [name]
href = rp + current_local_href + qs_fn({"labels": new_sel}) href = prefix + current_local_href + qs_fn({"labels": new_sel})
else: else:
href = "#" href = "#"
ring = " ring-2 ring-emerald-500 rounded" if is_sel else "" ring = " ring-2 ring-emerald-500 rounded" if is_sel else ""
src = asset_url_fn(f"{prefix}/{name}.svg") if callable(asset_url_fn) else "" src = asset_url_fn(f"{img_prefix}/{name}.svg") if callable(asset_url_fn) else ""
item_parts.append(sx_call( items.append({"href": href, "hx-select": hx_select,
"market-filter-label-item", "ring-cls": ring, "src": src, "name": name})
href=href, hx_select=hx_select, ring_cls=ring, src=src, name=name, return items
))
return "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
def _stickers_filter_sx(stickers: list, selected: list, ctx: dict, mobile: bool = False) -> str: def _sticker_data(stickers: list, selected: list, ctx: dict) -> list:
"""Build sticker filter grid as sx.""" """Extract sticker filter data for .sx composition."""
asset_url_fn = ctx.get("asset_url") prefix, current_local_href, hx_select, qs_fn, asset_url_fn = _filter_common(ctx)
current_local_href = ctx.get("current_local_href", "/") items = []
hx_select = ctx.get("hx_select_search", "#main-panel")
qs_fn = ctx.get("qs_filter")
from shared.utils import route_prefix
rp = route_prefix()
item_parts: list[str] = []
for st in stickers: for st in stickers:
name = st.get("name", "") name = st.get("name", "")
count = st.get("count", 0) count = st.get("count", 0)
is_sel = name in selected is_sel = name in selected
if callable(qs_fn): if callable(qs_fn):
new_sel = [s for s in selected if s != name] if is_sel else selected + [name] new_sel = [s for s in selected if s != name] if is_sel else selected + [name]
href = rp + current_local_href + qs_fn({"stickers": new_sel}) href = prefix + current_local_href + qs_fn({"stickers": new_sel})
else: else:
href = "#" href = "#"
ring = " ring-2 ring-emerald-500 rounded" if is_sel else "" ring = " ring-2 ring-emerald-500 rounded" if is_sel else ""
src = asset_url_fn(f"stickers/{name}.svg") if callable(asset_url_fn) else "" src = asset_url_fn(f"stickers/{name}.svg") if callable(asset_url_fn) else ""
cls = "text-[10px] text-stone-500" if count != 0 else "text-md text-red-500 font-bold" cls = "text-[10px] text-stone-500" if count != 0 else "text-md text-red-500 font-bold"
item_parts.append(sx_call( items.append({"href": href, "hx-select": hx_select, "ring-cls": ring,
"market-filter-sticker-item", "src": src, "name": name, "count-cls": cls, "count": str(count)})
href=href, hx_select=hx_select, ring_cls=ring, return items
src=src, name=name, count_cls=cls, count=str(count),
))
items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
return sx_call("market-filter-stickers-row", items=SxExpr(items_sx))
def _brand_filter_sx(brands: list, selected: list, ctx: dict, mobile: bool = False) -> str: def _brand_data(brands: list, selected: list, ctx: dict) -> list:
"""Build brand filter checkboxes as sx.""" """Extract brand filter data for .sx composition."""
current_local_href = ctx.get("current_local_href", "/") prefix, current_local_href, hx_select, qs_fn, _ = _filter_common(ctx)
hx_select = ctx.get("hx_select_search", "#main-panel") items = []
qs_fn = ctx.get("qs_filter")
from shared.utils import route_prefix
rp = route_prefix()
item_parts: list[str] = []
for br in brands: for br in brands:
name = br.get("name", "") name = br.get("name", "")
count = br.get("count", 0) count = br.get("count", 0)
is_sel = name in selected is_sel = name in selected
if callable(qs_fn): if callable(qs_fn):
new_sel = [s for s in selected if s != name] if is_sel else selected + [name] new_sel = [s for s in selected if s != name] if is_sel else selected + [name]
href = rp + current_local_href + qs_fn({"brands": new_sel}) href = prefix + current_local_href + qs_fn({"brands": new_sel})
else: else:
href = "#" href = "#"
bg = " bg-yellow-200" if is_sel else "" bg = " bg-yellow-200" if is_sel else ""
cls = "text-md" if count else "text-md text-red-500" cls = "text-md" if count else "text-md text-red-500"
item_parts.append(sx_call( items.append({"href": href, "hx-select": hx_select, "bg-cls": bg,
"market-filter-brand-item", "name-cls": cls, "name": name, "count": str(count)})
href=href, hx_select=hx_select, bg_cls=bg, return items
name_cls=cls, name=name, count=str(count),
))
items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
return sx_call("market-filter-brands-panel", items=SxExpr(items_sx))
def _subcategory_selector_sx(subs: list, top_href: str, current_sub: str, ctx: dict) -> str: def _subcategory_data(subs: list, top_href: str, current_sub: str, ctx: dict) -> dict:
"""Build subcategory vertical nav as sx.""" """Extract subcategory filter data for .sx composition."""
hx_select = ctx.get("hx_select_search", "#main-panel")
from shared.utils import route_prefix from shared.utils import route_prefix
rp = route_prefix() rp = route_prefix()
hx_select = ctx.get("hx_select_search", "#main-panel")
all_cls = " bg-stone-200 font-medium" if not current_sub else "" items = []
all_full_href = rp + top_href
item_parts = [sx_call(
"market-filter-subcategory-item",
href=all_full_href, hx_select=hx_select, active_cls=all_cls, name="All",
)]
for sub in subs: for sub in subs:
slug = sub.get("slug", "") slug = sub.get("slug", "")
name = sub.get("name", "") active = slug == current_sub
href = sub.get("href", "")
active = (slug == current_sub)
active_cls = " bg-stone-200 font-medium" if active else "" active_cls = " bg-stone-200 font-medium" if active else ""
full_href = rp + href items.append({"href": rp + sub.get("href", ""), "hx-select": hx_select,
item_parts.append(sx_call( "active-cls": active_cls, "name": sub.get("name", "")})
"market-filter-subcategory-item", return {"items": items, "all-href": rp + top_href, "current-sub": current_sub}
href=full_href, hx_select=hx_select, active_cls=active_cls, name=name,
))
items_sx = "(<> " + " ".join(item_parts) + ")" # ---------------------------------------------------------------------------
return sx_call("market-filter-subcategory-panel", items=SxExpr(items_sx)) # Desktop filter panel
# ---------------------------------------------------------------------------
async def _desktop_filter_sx(ctx: dict) -> str:
"""Build the desktop aside filter panel — delegates to .sx defcomp."""
hx_select = ctx.get("hx_select_search", "#main-panel")
# Search (still uses render_to_sx for shared component)
search_sx = await search_desktop_sx(ctx)
# Sort data
sort_options = ctx.get("sort_options", [])
sort = ctx.get("sort", "")
sd = _sort_data(sort_options, sort, ctx) if sort_options else None
# Like data
ld = _like_data(ctx.get("liked", False), ctx.get("liked_count", 0), ctx)
# Labels data
labels = ctx.get("labels", [])
selected_labels = ctx.get("selected_labels", [])
lab_d = _label_data(labels, selected_labels, ctx) if labels else None
# Stickers data
stickers = ctx.get("stickers", [])
selected_stickers = ctx.get("selected_stickers", [])
st_d = _sticker_data(stickers, selected_stickers, ctx) if stickers else None
# Brands data
brands = ctx.get("brands", [])
selected_brands = ctx.get("selected_brands", [])
br_d = _brand_data(brands, selected_brands, ctx) if brands else None
# Subcategory data
subs_local = ctx.get("subs_local", [])
top_local_href = ctx.get("top_local_href", "")
sub_slug = ctx.get("sub_slug", "")
sub_d = (_subcategory_data(subs_local, top_local_href, sub_slug, ctx)
if subs_local and top_local_href else None)
return sx_call("market-desktop-filter-from-data",
search_sx=SxExpr(search_sx),
category_label=ctx.get("category_label", ""),
sort_data=sd,
like_data=ld,
label_data=lab_d,
sticker_data=st_d,
brand_data=br_d,
sub_data=sub_d,
hx_select=hx_select)
# ---------------------------------------------------------------------------
# Mobile filter summary
# ---------------------------------------------------------------------------
def _mobile_chips_data(ctx: dict) -> dict:
"""Extract mobile filter chip data for .sx composition."""
asset_url_fn = ctx.get("asset_url")
sort = ctx.get("sort", "")
sort_options = ctx.get("sort_options", [])
liked = ctx.get("liked", False)
liked_count = ctx.get("liked_count", 0)
selected_labels = ctx.get("selected_labels", [])
selected_stickers = ctx.get("selected_stickers", [])
selected_brands = ctx.get("selected_brands", [])
labels = ctx.get("labels", [])
stickers = ctx.get("stickers", [])
brands = ctx.get("brands", [])
# Sort chip
sort_chip = None
if sort and sort_options:
for k, l, i in sort_options:
if k == sort and callable(asset_url_fn):
sort_chip = {"src": asset_url_fn(i), "label": l}
# Liked chip
liked_chip = None
if liked:
count_cls = ("text-[10px] text-stone-500" if liked_count != 0
else "text-md text-red-500 font-bold")
liked_chip = {"count": str(liked_count) if liked_count is not None else None,
"count-cls": count_cls}
# Label chips
label_chips = []
if selected_labels:
for sl in selected_labels:
for lb in labels:
if lb.get("name") == sl and callable(asset_url_fn):
chip: dict = {"src": asset_url_fn("nav-labels/" + sl + ".svg"), "name": sl}
if lb.get("count") is not None:
cls = ("text-[10px] text-stone-500" if lb["count"] != 0
else "text-md text-red-500 font-bold")
chip["count"] = str(lb["count"])
chip["count-cls"] = cls
label_chips.append(chip)
# Sticker chips
sticker_chips = []
if selected_stickers:
for ss in selected_stickers:
for st in stickers:
if st.get("name") == ss and callable(asset_url_fn):
chip = {"src": asset_url_fn("stickers/" + ss + ".svg"), "name": ss}
if st.get("count") is not None:
cls = ("text-[10px] text-stone-500" if st["count"] != 0
else "text-md text-red-500 font-bold")
chip["count"] = str(st["count"])
chip["count-cls"] = cls
sticker_chips.append(chip)
# Brand chips
brand_chips = []
if selected_brands:
for b in selected_brands:
count = 0
for br in brands:
if br.get("name") == b:
count = br.get("count", 0)
brand_chips.append({"name": b, "count": str(count), "has-count": bool(count)})
return {
"sort-chip": sort_chip,
"liked-chip": liked_chip,
"label-chips": label_chips or None,
"sticker-chips": sticker_chips or None,
"brand-chips": brand_chips or None,
}
def _mobile_filter_content_data(ctx: dict) -> dict:
"""Extract mobile filter expanded panel data."""
from shared.utils import route_prefix
prefix = route_prefix()
hx_select = ctx.get("hx_select_search", "#main-panel")
sort_options = ctx.get("sort_options", [])
sort = ctx.get("sort", "")
sd = _sort_data(sort_options, sort, ctx) if sort_options else None
ld = _like_data(ctx.get("liked", False), ctx.get("liked_count", 0), ctx)
labels = ctx.get("labels", [])
selected_labels = ctx.get("selected_labels", [])
lab_d = _label_data(labels, selected_labels, ctx) if labels else None
stickers = ctx.get("stickers", [])
selected_stickers = ctx.get("selected_stickers", [])
st_d = _sticker_data(stickers, selected_stickers, ctx) if stickers else None
brands = ctx.get("brands", [])
selected_brands = ctx.get("selected_brands", [])
br_d = _brand_data(brands, selected_brands, ctx) if brands else None
# Clear filters URL
clear_href = None
search = ctx.get("search", "")
has_filters = search or selected_labels or selected_stickers or selected_brands
qs_fn = ctx.get("qs_filter")
if has_filters and callable(qs_fn):
current_local_href = ctx.get("current_local_href", "/")
clear_href = prefix + current_local_href + qs_fn({"clear_filters": True})
return {
"sort-data": sd,
"like-data": ld,
"label-data": lab_d,
"sticker-data": st_d,
"brand-data": br_d,
"clear-href": clear_href,
"hx-select": hx_select,
}
async def _mobile_filter_summary_sx(ctx: dict) -> str:
"""Build mobile filter summary — delegates to .sx defcomps."""
# Search bar (still uses render_to_sx for shared component)
search_bar = await search_mobile_sx(ctx)
# Chips data
chips_data = _mobile_chips_data(ctx)
chips = sx_call("market-mobile-chips-from-data", **chips_data)
# Expanded filter content data
filter_data = _mobile_filter_content_data(ctx)
filter_content = sx_call("market-mobile-filter-content-from-data", **filter_data)
return sx_call("market-mobile-filter-summary",
search_bar=SxExpr(search_bar),
chips=SxExpr(chips),
filter=SxExpr(filter_content))

View File

@@ -1,274 +1,197 @@
"""Layout registration + header builders.""" """Layout registration + header data builders."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import sx_call from shared.sx.helpers import sx_call
from .utils import _set_prices, _price_str from .utils import _set_prices, _price_str
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Header helpers # Header data extraction — pure data, no component references
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _market_header_sx(ctx: dict, *, oob: bool = False) -> str: def _market_header_data(ctx: dict) -> dict:
"""Build the market-level header row as sx call string.""" """Extract market header data for .sx composition."""
from quart import url_for
market_title = ctx.get("market_title", "")
top_slug = ctx.get("top_slug", "")
sub_slug = ctx.get("sub_slug", "")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
label_sx = sx_call(
"market-shop-label",
title=market_title, top_slug=top_slug or "",
sub_div=sub_slug or None,
)
link_href = url_for("defpage_market_home")
# Build desktop nav from categories
categories = ctx.get("categories", {})
qs = ctx.get("qs", "")
nav_sx = _desktop_category_nav_sx(ctx, categories, qs, hx_select_search)
return sx_call(
"menu-row-sx",
id="market-row", level=2,
link_href=link_href, link_label_content=label_sx,
nav=nav_sx or None,
child_id="market-header-child", oob=oob,
)
def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str,
hx_select: str) -> str:
"""Build desktop category navigation links as sx."""
from quart import url_for from quart import url_for
from shared.utils import route_prefix from shared.utils import route_prefix
prefix = route_prefix() prefix = route_prefix()
category_label = ctx.get("category_label", "") hx_select = ctx.get("hx_select_search", "#main-panel")
select_colours = ctx.get("select_colours", "") select_colours = ctx.get("select_colours", "")
rights = ctx.get("rights", {}) rights = ctx.get("rights", {})
qs = ctx.get("qs", "")
categories = ctx.get("categories", {})
all_href = prefix + url_for("market.browse.browse_all") + qs all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
link_parts = [sx_call(
"market-category-link",
href=all_href, hx_select=hx_select, active=all_active,
select_colours=select_colours, label="All",
)]
cat_data = []
for cat, data in categories.items(): for cat, data in categories.items():
cat_href = prefix + url_for("market.browse.browse_top", top_slug=data["slug"]) + qs cat_href = prefix + url_for("market.browse.browse_top", top_slug=data["slug"]) + qs
cat_active = (cat == category_label) cat_data.append({
link_parts.append(sx_call( "href": cat_href,
"market-category-link", "active": cat == ctx.get("category_label", ""),
href=cat_href, hx_select=hx_select, active=cat_active, "label": cat,
select_colours=select_colours, label=cat, })
))
links_sx = "(<> " + " ".join(link_parts) + ")" admin_href = ""
admin_sx = ""
if rights and rights.get("admin"): if rights and rights.get("admin"):
admin_href = prefix + url_for("defpage_market_admin") admin_href = prefix + url_for("defpage_market_admin")
admin_sx = sx_call("market-admin-link", href=admin_href, hx_select=hx_select)
return sx_call("market-desktop-category-nav", return {
links=SxExpr(links_sx), "market-title": ctx.get("market_title", ""),
admin=admin_sx or None) "top-slug": ctx.get("top_slug", ""),
"sub-slug": ctx.get("sub_slug", ""),
"link-href": url_for("defpage_market_home"),
"categories": cat_data,
"hx-select": hx_select,
"select-colours": select_colours,
"all-href": all_href,
"all-active": ctx.get("category_label", "") == "All Products",
"admin-href": admin_href,
}
def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str: def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the product-level header row as sx call string.""" """Build market header as sx — delegates to .sx defcomp."""
data = _market_header_data(ctx)
return sx_call("market-header-from-data", oob=oob, **data)
def _product_header_data(ctx: dict, d: dict) -> dict:
"""Extract product header data for .sx composition."""
from quart import url_for from quart import url_for
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
slug = d.get("slug", "") slug = d.get("slug", "")
title = d.get("title", "") hx_select = ctx.get("hx_select_search", "#main-panel")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
link_href = url_for("market.browse.product.product_detail", product_slug=slug)
label_sx = sx_call("market-product-label", title=title)
# Prices in nav area
pr = _set_prices(d)
cart = ctx.get("cart", []) cart = ctx.get("cart", [])
prices_nav = _prices_header_sx(d, pr, cart, slug, ctx)
rights = ctx.get("rights", {}) rights = ctx.get("rights", {})
nav_parts = [prices_nav]
if rights and rights.get("admin"):
admin_href = url_for("market.browse.product.admin", product_slug=slug)
nav_parts.append(sx_call("market-admin-link", href=admin_href, hx_select=hx_select_search))
nav_sx = "(<> " + " ".join(nav_parts) + ")"
return sx_call( # Price data
"menu-row-sx", pr = _set_prices(d)
id="product-row", level=3, sp_str = _price_str(pr["sp_val"], pr["sp_raw"], pr["sp_cur"]) if pr["sp_val"] else ""
link_href=link_href, link_label_content=label_sx, rp_str = _price_str(pr["rp_val"], pr["rp_raw"], pr["rp_cur"]) if pr["rp_val"] else ""
nav=SxExpr(nav_sx), child_id="product-header-child", oob=oob,
)
def _prices_header_sx(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> str:
"""Build prices + add-to-cart for product header row as sx."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
csrf = generate_csrf_token()
cart_action = url_for("market.browse.product.cart", product_slug=slug)
cart_url_fn = ctx.get("cart_url")
# Add-to-cart button
quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0
add_sx = _cart_add_sx(slug, quantity, cart_action, csrf, cart_url_fn)
parts = [add_sx]
sp_val, rp_val = pr.get("sp_val"), pr.get("rp_val")
if sp_val:
parts.append(sx_call("market-header-price-special-label"))
parts.append(sx_call("market-header-price-special",
price=_price_str(sp_val, pr["sp_raw"], pr["sp_cur"])))
if rp_val:
parts.append(sx_call("market-header-price-strike",
price=_price_str(rp_val, pr["rp_raw"], pr["rp_cur"])))
elif rp_val:
parts.append(sx_call("market-header-price-regular-label"))
parts.append(sx_call("market-header-price-regular",
price=_price_str(rp_val, pr["rp_raw"], pr["rp_cur"])))
# RRP # RRP
rrp_str = ""
rrp_raw = d.get("rrp_raw") rrp_raw = d.get("rrp_raw")
rrp_val = d.get("rrp") rrp_val = d.get("rrp")
case_size = d.get("case_size_count") or 1 case_size = d.get("case_size_count") or 1
if rrp_raw and rrp_val: if rrp_raw and rrp_val:
rrp_str = f"{rrp_raw[0]}{rrp_val * case_size:.2f}" rrp_str = f"{rrp_raw[0]}{rrp_val * case_size:.2f}"
parts.append(sx_call("market-header-rrp", rrp=rrp_str))
inner_sx = "(<> " + " ".join(parts) + ")"
return sx_call("market-prices-row", inner=SxExpr(inner_sx))
def _cart_add_sx(slug: str, quantity: int, action: str, csrf: str,
cart_url_fn: Any = None) -> str:
"""Build add-to-cart button or quantity controls as sx."""
if not quantity:
return sx_call(
"market-cart-add-empty",
cart_id=f"cart-{slug}", action=action, csrf=csrf,
)
# Cart state
csrf = generate_csrf_token()
cart_action = url_for("market.browse.product.cart", product_slug=slug)
quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0
cart_url_fn = ctx.get("cart_url")
cart_href = cart_url_fn("/") if callable(cart_url_fn) else "/" cart_href = cart_url_fn("/") if callable(cart_url_fn) else "/"
return sx_call(
"market-cart-add-quantity", admin_href = ""
cart_id=f"cart-{slug}", action=action, csrf=csrf, if rights and rights.get("admin"):
minus_val=str(quantity - 1), plus_val=str(quantity + 1), admin_href = url_for("market.browse.product.admin", product_slug=slug)
quantity=str(quantity), cart_href=cart_href,
) return {
"title": d.get("title", ""),
"link-href": url_for("market.browse.product.product_detail", product_slug=slug),
"hx-select": hx_select,
"price-data": {
"cart-id": f"cart-{slug}",
"cart-action": cart_action,
"csrf": csrf,
"quantity": quantity,
"cart-href": cart_href,
"sp-val": pr["sp_val"] or "",
"sp-str": sp_str,
"rp-val": pr["rp_val"] or "",
"rp-str": rp_str,
"rrp-str": rrp_str,
},
"admin-href": admin_href,
}
def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product header as sx — delegates to .sx defcomp."""
data = _product_header_data(ctx, d)
return sx_call("market-product-header-from-data", oob=oob, **data)
def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product admin header as sx — delegates to .sx defcomp."""
from quart import url_for
slug = d.get("slug", "")
link_href = url_for("market.browse.product.admin", product_slug=slug)
return sx_call("market-product-admin-header-from-data",
link_href=link_href, oob=oob)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Mobile nav panel # Mobile nav panel data extraction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _mobile_nav_panel_sx(ctx: dict) -> str: def _mobile_nav_data(ctx: dict) -> dict:
"""Build mobile nav panel with category accordion as sx.""" """Extract mobile nav panel data for .sx composition."""
from quart import url_for from quart import url_for
from shared.utils import route_prefix from shared.utils import route_prefix
prefix = route_prefix() prefix = route_prefix()
categories = ctx.get("categories", {}) categories = ctx.get("categories", {})
qs = ctx.get("qs", "") qs = ctx.get("qs", "")
category_label = ctx.get("category_label", "")
top_slug = ctx.get("top_slug", "") top_slug = ctx.get("top_slug", "")
sub_slug = ctx.get("sub_slug", "") sub_slug = ctx.get("sub_slug", "")
hx_select = ctx.get("hx_select_search", "#main-panel") hx_select = ctx.get("hx_select_search", "#main-panel")
select_colours = ctx.get("select_colours", "") select_colours = ctx.get("select_colours", "")
all_href = prefix + url_for("market.browse.browse_all") + qs all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
item_parts = [sx_call(
"market-mobile-all-link",
href=all_href, hx_select=hx_select, active=all_active,
select_colours=select_colours,
)]
cat_data = []
for cat, data in categories.items(): for cat, data in categories.items():
cat_slug = data.get("slug", "") cat_slug = data.get("slug", "")
cat_active = (top_slug == cat_slug.lower() if top_slug else False) cat_active = top_slug == cat_slug.lower() if top_slug else False
cat_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs cat_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs
bg_cls = " bg-stone-900 text-white hover:bg-stone-900" if cat_active else ""
chevron_sx = sx_call("market-mobile-chevron")
cat_count = data.get("count", 0)
summary_sx = sx_call(
"market-mobile-cat-summary",
bg_cls=bg_cls, href=cat_href, hx_select=hx_select,
select_colours=select_colours, cat_name=cat,
count_label=f"{cat_count} products", count_str=str(cat_count),
chevron=chevron_sx,
)
subs = data.get("subs", []) subs = data.get("subs", [])
subs_sx = "" sub_data = []
if subs: if subs:
sub_link_parts = []
for sub in subs: for sub in subs:
sub_href = prefix + url_for("market.browse.browse_sub", top_slug=cat_slug, sub_slug=sub["slug"]) + qs sub_href = prefix + url_for("market.browse.browse_sub",
sub_active = (cat_active and sub_slug == sub.get("slug")) top_slug=cat_slug, sub_slug=sub["slug"]) + qs
sub_active = cat_active and sub_slug == sub.get("slug")
sub_label = sub.get("html_label") or sub.get("name", "") sub_label = sub.get("html_label") or sub.get("name", "")
sub_count = sub.get("count", 0) sub_data.append({
sub_link_parts.append(sx_call( "href": sub_href,
"market-mobile-sub-link", "active": sub_active,
select_colours=select_colours, active=sub_active, "label": sub_label,
href=sub_href, hx_select=hx_select, label=sub_label, "count": sub.get("count", 0),
count_label=f"{sub_count} products", count_str=str(sub_count), })
))
sub_links_sx = "(<> " + " ".join(sub_link_parts) + ")"
subs_sx = sx_call("market-mobile-subs-panel", links=SxExpr(sub_links_sx))
else:
view_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs
subs_sx = sx_call("market-mobile-view-all", href=view_href, hx_select=hx_select)
item_parts.append(sx_call( cat_data.append({
"market-mobile-cat-details", "name": cat,
open=cat_active or None, "href": cat_href,
summary=summary_sx, "active": cat_active,
subs=subs_sx, "count": data.get("count", 0),
)) "subs": sub_data if sub_data else None,
})
items_sx = "(<> " + " ".join(item_parts) + ")" return {
return sx_call("market-mobile-nav-wrapper", items=SxExpr(items_sx)) "categories": cat_data,
"all-href": all_href,
"all-active": ctx.get("category_label", "") == "All Products",
"hx-select": hx_select,
"select-colours": select_colours,
}
def _mobile_nav_panel_sx(ctx: dict) -> str:
"""Build mobile nav panel as sx — delegates to .sx defcomp."""
data = _mobile_nav_data(ctx)
return sx_call("market-mobile-nav-from-data", **data)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Product admin header
# ---------------------------------------------------------------------------
def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product admin header row as sx."""
from quart import url_for
slug = d.get("slug", "")
link_href = url_for("market.browse.product.admin", product_slug=slug)
return sx_call(
"menu-row-sx",
id="product-admin-row", level=4,
link_href=link_href, link_label="admin!!", icon="fa fa-cog",
child_id="product-admin-header-child", oob=oob,
)
# ===========================================================================
# Layout registration — all layouts delegate to .sx defcomps # Layout registration — all layouts delegate to .sx defcomps
# =========================================================================== # ---------------------------------------------------------------------------
def _register_market_layouts() -> None: def _register_market_layouts() -> None:
from shared.sx.layouts import register_sx_layout from shared.sx.layouts import register_sx_layout

View File

@@ -11,7 +11,7 @@ from shared.sx.helpers import (
full_page_sx, oob_page_sx, full_page_sx, oob_page_sx,
) )
from .utils import _set_prices, _price_str, _clear_deeper_oob, _product_detail_sx, _product_meta_sx from .utils import _clear_deeper_oob, _product_detail_sx, _product_meta_sx
from .cards import _product_cards_sx, _market_cards_sx from .cards import _product_cards_sx, _market_cards_sx
from .filters import _desktop_filter_sx, _mobile_filter_summary_sx from .filters import _desktop_filter_sx, _mobile_filter_summary_sx
from .layouts import ( from .layouts import (
@@ -36,9 +36,7 @@ async def render_browse_page(ctx: dict) -> str:
content = _product_grid(cards) content = _product_grid(cards)
from shared.sx.helpers import render_to_sx_with_env from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-browse-layout-full", {}, hdr = await render_to_sx_with_env("market-browse-layout-full", {})
post_header=await _post_header_sx(ctx),
market_header=_market_header_sx(ctx))
menu = _mobile_nav_panel_sx(ctx) menu = _mobile_nav_panel_sx(ctx)
filter_sx = await _mobile_filter_summary_sx(ctx) filter_sx = await _mobile_filter_summary_sx(ctx)
aside_sx = await _desktop_filter_sx(ctx) aside_sx = await _desktop_filter_sx(ctx)
@@ -52,13 +50,8 @@ async def render_browse_oob(ctx: dict) -> str:
cards = _product_cards_sx(ctx) cards = _product_cards_sx(ctx)
content = _product_grid(cards) content = _product_grid(cards)
oob_hdr = await _oob_header_sx("post-header-child", "market-header-child", # Layout handles all OOB headers via auto-fetch macros
_market_header_sx(ctx)) oobs = sx_call("market-browse-layout-oob")
oobs = sx_call("market-browse-layout-oob",
oob_header=oob_hdr,
post_header_oob=await _post_header_sx(ctx, oob=True),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child")))
menu = _mobile_nav_panel_sx(ctx) menu = _mobile_nav_panel_sx(ctx)
filter_sx = await _mobile_filter_summary_sx(ctx) filter_sx = await _mobile_filter_summary_sx(ctx)
aside_sx = await _desktop_filter_sx(ctx) aside_sx = await _desktop_filter_sx(ctx)
@@ -207,7 +200,7 @@ def render_cart_added_response(cart: list, item: Any, d: dict) -> str:
Returns OOB fragments: cart-mini icon + product add/remove buttons + cart item row. Returns OOB fragments: cart-mini icon + product add/remove buttons + cart item row.
""" """
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
from quart import url_for, g from quart import url_for
from shared.infrastructure.urls import cart_url as _cart_url from shared.infrastructure.urls import cart_url as _cart_url
csrf = generate_csrf_token() csrf = generate_csrf_token()

View File

@@ -1,9 +1,6 @@
"""Price helpers, OOB helpers, product detail/meta builders.""" """Price helpers, OOB helpers, product detail/meta data builders."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import sx_call from shared.sx.helpers import sx_call
@@ -54,31 +51,14 @@ def _set_prices(item: dict) -> dict:
rp_val=rp_val, rp_raw=rp_raw, rp_cur=rp_cur) rp_val=rp_val, rp_raw=rp_raw, rp_cur=rp_cur)
def _card_price_sx(p: dict) -> str:
"""Build price line for product card as sx call."""
pr = _set_prices(p)
sp_str = _price_str(pr["sp_val"], pr["sp_raw"], pr["sp_cur"])
rp_str = _price_str(pr["rp_val"], pr["rp_raw"], pr["rp_cur"])
parts: list[str] = []
if pr["sp_val"]:
parts.append(sx_call("market-price-special", price=sp_str))
if pr["rp_val"]:
parts.append(sx_call("market-price-regular-strike", price=rp_str))
elif pr["rp_val"]:
parts.append(sx_call("market-price-regular", price=rp_str))
inner = "(<> " + " ".join(parts) + ")" if parts else None
return sx_call("market-price-line", inner=SxExpr(inner) if inner else None)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Product detail page content # Product detail data extraction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _product_detail_sx(d: dict, ctx: dict) -> str: def _product_detail_data(d: dict, ctx: dict) -> dict:
"""Build product detail main panel content as sx.""" """Extract product detail page data for .sx composition."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
from .cards import _like_button_sx from .cards import _like_button_data
asset_url_fn = ctx.get("asset_url") asset_url_fn = ctx.get("asset_url")
user = ctx.get("user") user = ctx.get("user")
@@ -91,132 +71,70 @@ def _product_detail_sx(d: dict, ctx: dict) -> str:
brand = d.get("brand", "") brand = d.get("brand", "")
slug = d.get("slug", "") slug = d.get("slug", "")
# Gallery # Like button data
if images: like_data = None
# Like button if user:
like_sx = "" like_data = _like_button_data(slug, liked_by_current_user, csrf, ctx)
if user:
like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx)
# Main image + labels # Label overlay URLs
label_parts: list[str] = [] label_urls = []
if callable(asset_url_fn): if callable(asset_url_fn):
for l in labels: label_urls = [asset_url_fn("labels/" + l + ".svg") for l in labels]
label_parts.append(sx_call(
"market-label-overlay",
src=asset_url_fn("labels/" + l + ".svg"),
))
labels_sx = "(<> " + " ".join(label_parts) + ")" if label_parts else None
gallery_inner = sx_call( # Image data
"market-detail-gallery-inner", image_data = [{"src": u, "alt": d.get("title", "")} for u in images] if images else []
like=like_sx or None,
image=images[0], alt=d.get("title", ""),
labels=SxExpr(labels_sx) if labels_sx else None,
brand=brand,
)
# Prev/next buttons # Thumbnail data
nav_buttons = "" thumb_data = []
if len(images) > 1: if len(images) > 1:
nav_buttons = sx_call("market-detail-nav-buttons") for i, u in enumerate(images):
thumb_data.append({"title": f"Image {i+1}", "src": u, "alt": f"thumb {i+1}"})
gallery_sx = sx_call( # Sticker items
"market-detail-gallery", sticker_items = []
inner=gallery_inner,
nav=nav_buttons or None,
)
# Thumbnails
gallery_parts = [gallery_sx]
if len(images) > 1:
thumb_parts = []
for i, u in enumerate(images):
thumb_parts.append(sx_call(
"market-detail-thumb",
title=f"Image {i+1}", src=u, alt=f"thumb {i+1}",
))
thumbs_sx = "(<> " + " ".join(thumb_parts) + ")"
gallery_parts.append(sx_call("market-detail-thumbs", thumbs=SxExpr(thumbs_sx)))
gallery_final = "(<> " + " ".join(gallery_parts) + ")"
else:
like_sx = ""
if user:
like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx)
gallery_final = sx_call("market-detail-no-image",
like=like_sx or None)
# Stickers below gallery
stickers_sx = ""
if stickers and callable(asset_url_fn): if stickers and callable(asset_url_fn):
sticker_parts = []
for s in stickers: for s in stickers:
sticker_parts.append(sx_call( sticker_items.append({"src": asset_url_fn("stickers/" + s + ".svg"), "name": s})
"market-detail-sticker",
src=asset_url_fn("stickers/" + s + ".svg"), name=s,
))
sticker_items_sx = "(<> " + " ".join(sticker_parts) + ")"
stickers_sx = sx_call("market-detail-stickers", items=SxExpr(sticker_items_sx))
# Right column: prices, description, sections # Extras (unit price, case size)
pr = _set_prices(d) extras = []
detail_parts: list[str] = []
# Unit price / case size extras
extra_parts: list[str] = []
ppu = d.get("price_per_unit") or d.get("price_per_unit_raw") ppu = d.get("price_per_unit") or d.get("price_per_unit_raw")
if ppu: if ppu:
extra_parts.append(sx_call( extras.append({
"market-detail-unit-price", "type": "unit-price",
price=_price_str(d.get("price_per_unit"), d.get("price_per_unit_raw"), d.get("price_per_unit_currency")), "value": _price_str(d.get("price_per_unit"), d.get("price_per_unit_raw"),
)) d.get("price_per_unit_currency")),
})
if d.get("case_size_raw"): if d.get("case_size_raw"):
extra_parts.append(sx_call("market-detail-case-size", size=d["case_size_raw"])) extras.append({"type": "case-size", "value": d["case_size_raw"]})
if extra_parts:
extras_sx = "(<> " + " ".join(extra_parts) + ")"
detail_parts.append(sx_call("market-detail-extras", inner=SxExpr(extras_sx)))
# Description return {
desc_short = d.get("description_short") "images": image_data or None,
desc_html_val = d.get("description_html") "labels": label_urls or None,
if desc_short or desc_html_val: "brand": brand,
desc_parts: list[str] = [] "like-data": like_data,
if desc_short: "has-nav-buttons": len(images) > 1,
desc_parts.append(sx_call("market-detail-desc-short", text=desc_short)) "thumbs": thumb_data or None,
if desc_html_val: "sticker-items": sticker_items or None,
desc_parts.append(sx_call("market-detail-desc-html", html=desc_html_val)) "extras": extras or None,
desc_inner = "(<> " + " ".join(desc_parts) + ")" "desc-short": d.get("description_short") or None,
detail_parts.append(sx_call("market-detail-desc-wrapper", inner=SxExpr(desc_inner))) "desc-html": d.get("description_html") or None,
"sections": d.get("sections") or None,
}
# Sections (expandable)
sections = d.get("sections", [])
if sections:
sec_parts = []
for sec in sections:
sec_parts.append(sx_call(
"market-detail-section",
title=sec.get("title", ""), html=sec.get("html", ""),
))
sec_items_sx = "(<> " + " ".join(sec_parts) + ")"
detail_parts.append(sx_call("market-detail-sections", items=SxExpr(sec_items_sx)))
details_inner_sx = "(<> " + " ".join(detail_parts) + ")" if detail_parts else "(<>)" def _product_detail_sx(d: dict, ctx: dict) -> str:
details_sx = sx_call("market-detail-right-col", inner=SxExpr(details_inner_sx)) """Build product detail content — delegates to .sx defcomp."""
data = _product_detail_data(d, ctx)
return sx_call( return sx_call("market-product-detail-from-data", **data)
"market-detail-layout",
gallery=SxExpr(gallery_final),
stickers=stickers_sx or None,
details=details_sx,
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Product meta (OpenGraph, JSON-LD) # Product meta data extraction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _product_meta_sx(d: dict, ctx: dict) -> str: def _product_meta_data(d: dict, ctx: dict) -> dict:
"""Build product meta tags as sx (auto-hoisted to <head> by sx.js).""" """Extract product meta/SEO data for .sx composition."""
import json import json
from quart import request from quart import request
@@ -231,36 +149,8 @@ def _product_meta_sx(d: dict, ctx: dict) -> str:
brand = d.get("brand", "") brand = d.get("brand", "")
sku = d.get("sku", "") sku = d.get("sku", "")
price = d.get("special_price") or d.get("regular_price") or d.get("rrp") price = d.get("special_price") or d.get("regular_price") or d.get("rrp")
price_currency = d.get("special_price_currency") or d.get("regular_price_currency") or d.get("rrp_currency") price_currency = (d.get("special_price_currency") or d.get("regular_price_currency")
or d.get("rrp_currency"))
parts = [sx_call("market-meta-title", title=title)]
parts.append(sx_call("market-meta-description", description=description))
if canonical:
parts.append(sx_call("market-meta-canonical", href=canonical))
# OpenGraph
site_title = ctx.get("base_title", "")
parts.append(sx_call("market-meta-og", property="og:site_name", content=site_title))
parts.append(sx_call("market-meta-og", property="og:type", content="product"))
parts.append(sx_call("market-meta-og", property="og:title", content=title))
parts.append(sx_call("market-meta-og", property="og:description", content=description))
if canonical:
parts.append(sx_call("market-meta-og", property="og:url", content=canonical))
if image_url:
parts.append(sx_call("market-meta-og", property="og:image", content=image_url))
if price and price_currency:
parts.append(sx_call("market-meta-og", property="product:price:amount", content=f"{price:.2f}"))
parts.append(sx_call("market-meta-og", property="product:price:currency", content=price_currency))
if brand:
parts.append(sx_call("market-meta-og", property="product:brand", content=brand))
# Twitter
card_type = "summary_large_image" if image_url else "summary"
parts.append(sx_call("market-meta-twitter", name="twitter:card", content=card_type))
parts.append(sx_call("market-meta-twitter", name="twitter:title", content=title))
parts.append(sx_call("market-meta-twitter", name="twitter:description", content=description))
if image_url:
parts.append(sx_call("market-meta-twitter", name="twitter:image", content=image_url))
# JSON-LD # JSON-LD
jsonld = { jsonld = {
@@ -282,6 +172,21 @@ def _product_meta_sx(d: dict, ctx: dict) -> str:
"url": canonical, "url": canonical,
"availability": "https://schema.org/InStock", "availability": "https://schema.org/InStock",
} }
parts.append(sx_call("market-meta-jsonld", json=json.dumps(jsonld)))
return "(<> " + " ".join(parts) + ")" return {
"title": title,
"description": description,
"canonical": canonical or None,
"image-url": image_url or None,
"site-title": ctx.get("base_title", ""),
"brand": brand or None,
"price": f"{price:.2f}" if price and price_currency else None,
"price-currency": price_currency if price else None,
"jsonld-json": json.dumps(jsonld),
}
def _product_meta_sx(d: dict, ctx: dict) -> str:
"""Build product meta tags — delegates to .sx defcomp."""
data = _product_meta_data(d, ctx)
return sx_call("market-product-meta-from-data", **data)