Files
mono/market/sxc/pages/cards.py
giles e81d77437e 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>
2026-03-05 01:11:57 +00:00

218 lines
7.6 KiB
Python

"""Product/market card data builders."""
from __future__ import annotations
from typing import Any
from shared.sx.helpers import sx_call
from .utils import _set_prices, _price_str
from .filters import _MOBILE_SENTINEL_HS, _DESKTOP_SENTINEL_HS
# ---------------------------------------------------------------------------
# Product card data extraction
# ---------------------------------------------------------------------------
def _product_card_data(p: dict, ctx: dict) -> dict:
"""Extract data for a single product card."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
from shared.utils import route_prefix
prefix = route_prefix()
slug = p.get("slug", "")
item_href = prefix + url_for("market.browse.product.product_detail", product_slug=slug)
hx_select = ctx.get("hx_select_search", "#main-panel")
asset_url_fn = ctx.get("asset_url")
cart = ctx.get("cart", [])
selected_brands = ctx.get("selected_brands", [])
selected_stickers = ctx.get("selected_stickers", [])
search = ctx.get("search", "")
user = ctx.get("user")
csrf = generate_csrf_token()
# Price data
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"])
# Image labels as src URLs
labels = p.get("labels", [])
label_srcs = []
if p.get("image") and callable(asset_url_fn):
label_srcs = [asset_url_fn("labels/" + l + ".svg") for l in labels]
# Stickers as data
raw_stickers = p.get("stickers", [])
sticker_data = []
if raw_stickers and callable(asset_url_fn):
for s in raw_stickers:
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})
# Title highlighting
title = p.get("title", "")
has_highlight = False
search_pre = search_mid = search_post = ""
if search and search.lower() in title.lower():
idx = title.lower().index(search.lower())
has_highlight = True
search_pre = title[:idx]
search_mid = title[idx:idx + len(search)]
search_post = title[idx + len(search):]
# Cart
quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0
cart_action = url_for("market.browse.product.cart", product_slug=slug)
cart_url_fn = ctx.get("cart_url")
cart_href = cart_url_fn("/") if callable(cart_url_fn) else "/"
brand = p.get("brand", "")
brand_highlight = " bg-yellow-200" if brand in selected_brands else ""
d: dict[str, Any] = {
"href": item_href, "hx-select": hx_select, "slug": slug,
"image": p.get("image", ""), "brand": brand, "brand-highlight": brand_highlight,
"special-price": sp_str, "regular-price": rp_str,
"cart-action": cart_action, "quantity": quantity, "cart-href": cart_href, "csrf": csrf,
"title": title, "has-like": bool(user),
}
if label_srcs:
d["labels"] = label_srcs
elif labels:
d["labels"] = labels
if user:
d["liked"] = p.get("is_liked", False)
d["like-action"] = url_for("market.browse.product.like_toggle", product_slug=slug)
if sticker_data:
d["stickers"] = sticker_data
if has_highlight:
d["has-highlight"] = True
d["search-pre"] = search_pre
d["search-mid"] = search_mid
d["search-post"] = search_post
return d
def _product_cards_sx(ctx: dict) -> str:
"""S-expression wire format for product cards — delegates to .sx defcomp."""
from shared.utils import route_prefix
prefix = route_prefix()
products = ctx.get("products", [])
page = ctx.get("page", 1)
total_pages = ctx.get("total_pages", 1)
current_local_href = ctx.get("current_local_href", "/")
qs_fn = ctx.get("qs_filter")
product_data = [_product_card_data(p, ctx) for p in products]
next_url = ""
if page < total_pages:
if callable(qs_fn):
next_qs = qs_fn({"page": page + 1})
else:
next_qs = f"?page={page + 1}"
next_url = prefix + current_local_href + next_qs
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_data(slug: str, liked: bool, csrf: str, ctx: dict) -> dict:
"""Extract like button data."""
from quart import url_for
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"
return {
"form-id": f"like-{slug}",
"action": action,
"slug": slug,
"csrf": csrf,
"icon-cls": icon_cls,
}
# ---------------------------------------------------------------------------
# Market cards data extraction
# ---------------------------------------------------------------------------
def _market_card_data(market: Any, page_info: dict, *, show_page_badge: bool = True,
post_slug: str = "") -> dict:
"""Extract data for a single market card."""
from shared.infrastructure.urls import market_url
name = getattr(market, "name", "")
description = getattr(market, "description", "")
slug = getattr(market, "slug", "")
container_id = getattr(market, "container_id", None)
href = ""
badge_href = ""
badge_title = ""
if show_page_badge and page_info:
pi = page_info.get(container_id, {})
p_slug = pi.get("slug", "")
p_title = pi.get("title", "")
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:
p_slug = post_slug
href = market_url(f"/{post_slug}/{slug}/") if post_slug else ""
return {
"name": name,
"description": description,
"href": href,
"show-badge": show_page_badge,
"badge-href": badge_href,
"badge-title": badge_title,
}
def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool,
next_url: str, *, show_page_badge: bool = True,
post_slug: str = "") -> str:
"""Build market cards as sx — delegates to .sx defcomp."""
market_data = [_market_card_data(m, page_info, show_page_badge=show_page_badge,
post_slug=post_slug) for m in markets]
return sx_call("market-cards-content",
markets=market_data,
page=page,
has_more=has_more,
next_url=next_url)
def _markets_grid(cards_sx: str) -> str:
"""Wrap market cards in a grid as sx."""
from shared.sx.parser import SxExpr
return sx_call("market-markets-grid", cards=SxExpr(cards_sx))
def _no_markets_sx(message: str = "No markets available") -> str:
"""Empty state for markets as sx."""
return sx_call("empty-state", icon="fa fa-store", message=message,
cls="px-3 py-12 text-center text-stone-400")
def _market_landing_content_sx(post: dict) -> str:
"""Build market landing page content — delegates to .sx defcomp."""
return sx_call("market-landing-from-data",
excerpt=post.get("custom_excerpt") or None,
feature_image=post.get("feature_image") or None,
html=post.get("html") or None)