"""
Market service s-expression page components.
Renders market landing, browse (category/subcategory), product detail,
product admin, market admin, page markets, and all markets pages.
Called from route handlers in place of ``render_template()``.
"""
from __future__ import annotations
from typing import Any
from markupsafe import escape
from shared.sexp.jinja_bridge import sexp
from shared.sexp.helpers import (
call_url, get_asset_url, root_header_html,
search_mobile_html, search_desktop_html,
full_page, oob_page,
)
# ---------------------------------------------------------------------------
# Price helpers
# ---------------------------------------------------------------------------
_SYM = {"GBP": "£", "EUR": "€", "USD": "$"}
def _price_str(val, raw, cur) -> str:
if raw:
return str(raw)
if isinstance(val, (int, float)):
return f"{_SYM.get(cur, '')}{val:.2f}"
return str(val or "")
def _set_prices(item: dict) -> dict:
"""Extract price values from product dict (mirrors prices.html set_prices macro)."""
oe = item.get("oe_list_price") or {}
sp_val = item.get("special_price") or (oe.get("special") if oe else None)
sp_raw = item.get("special_price_raw") or (oe.get("special_raw") if oe else None)
sp_cur = item.get("special_price_currency") or (oe.get("special_currency") if oe else None)
rp_val = item.get("regular_price") or item.get("rrp") or (oe.get("rrp") if oe else None)
rp_raw = item.get("regular_price_raw") or item.get("rrp_raw") or (oe.get("rrp_raw") if oe else None)
rp_cur = item.get("regular_price_currency") or item.get("rrp_currency") or (oe.get("rrp_currency") if oe else None)
return dict(sp_val=sp_val, sp_raw=sp_raw, sp_cur=sp_cur,
rp_val=rp_val, rp_raw=rp_raw, rp_cur=rp_cur)
def _card_price_html(p: dict) -> str:
"""Render price line for product card (mirrors prices.html card_price macro)."""
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 = ['
']
if pr["sp_val"]:
parts.append(f'
{sp_str}
')
if pr["rp_val"]:
parts.append(f'
{rp_str}
')
elif pr["rp_val"]:
parts.append(f'
{rp_str}
')
parts.append("
")
return "".join(parts)
# ---------------------------------------------------------------------------
# Header helpers
# ---------------------------------------------------------------------------
def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the post-level header row (feature image + title + page cart count)."""
post = ctx.get("post") or {}
slug = post.get("slug", "")
title = (post.get("title") or "")[:160]
feature_image = post.get("feature_image")
label_parts = []
if feature_image:
label_parts.append(
f' '
)
label_parts.append(f"{escape(title)} ")
label_html = "".join(label_parts)
nav_parts = []
page_cart_count = ctx.get("page_cart_count", 0)
if page_cart_count and page_cart_count > 0:
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
nav_parts.append(
f''
f' '
f'{page_cart_count} '
)
# Container nav
container_nav = ctx.get("container_nav_html", "")
if container_nav:
nav_parts.append(container_nav)
nav_html = "".join(nav_parts)
link_href = call_url(ctx, "blog_url", f"/{slug}/")
return sexp(
'(~menu-row :id "post-row" :level 1'
' :link-href lh :link-label-html llh'
' :nav-html nh :child-id "post-header-child" :oob oob)',
lh=link_href,
llh=label_html,
nh=nav_html,
oob=oob,
)
def _market_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the market-level header row (shop icon + market title + category slugs + nav)."""
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_parts = [
'',
f'
{escape(market_title)}
',
'
',
f"
{escape(top_slug or '')}
",
]
if sub_slug:
label_parts.append(f"
{escape(sub_slug)}
")
label_parts.append("
")
label_html = "".join(label_parts)
link_href = url_for("market.browse.home")
# Build desktop nav from categories
categories = ctx.get("categories", {})
qs = ctx.get("qs", "")
nav_html = _desktop_category_nav_html(ctx, categories, qs, hx_select_search)
return sexp(
'(~menu-row :id "market-row" :level 2'
' :link-href lh :link-label-html llh'
' :nav-html nh :child-id "market-header-child" :oob oob)',
lh=link_href,
llh=label_html,
nh=nav_html,
oob=oob,
)
def _desktop_category_nav_html(ctx: dict, categories: dict, qs: str,
hx_select: str) -> str:
"""Build desktop category navigation links."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
category_label = ctx.get("category_label", "")
select_colours = ctx.get("select_colours", "")
rights = ctx.get("rights", {})
parts = ['']
all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
parts.append(
f''
)
for cat, data in categories.items():
cat_href = prefix + url_for("market.browse.browse_top", top_slug=data["slug"]) + qs
cat_active = (cat == category_label)
parts.append(
f''
)
# Admin link
if rights and rights.get("admin"):
admin_href = prefix + url_for("market.admin.admin")
parts.append(
f''
f' '
)
parts.append(" ")
return "".join(parts)
def _product_header_html(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build the product-level header row (bag icon + title + prices + admin)."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
slug = d.get("slug", "")
title = d.get("title", "")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
link_href = url_for("market.browse.product.product_detail", product_slug=slug)
label_html = f'{escape(title)}
'
# Prices in nav area
pr = _set_prices(d)
cart = ctx.get("cart", [])
prices_nav = _prices_header_html(d, pr, cart, slug, ctx)
rights = ctx.get("rights", {})
admin_html = ""
if rights and rights.get("admin"):
admin_href = url_for("market.browse.product.admin", product_slug=slug)
admin_html = (
f''
f' '
)
nav_html = prices_nav + admin_html
return sexp(
'(~menu-row :id "product-row" :level 3'
' :link-href lh :link-label-html llh'
' :nav-html nh :child-id "product-header-child" :oob oob)',
lh=link_href,
llh=label_html,
nh=nav_html,
oob=oob,
)
def _prices_header_html(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> str:
"""Build prices + add-to-cart for product header row."""
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_html = _cart_add_html(slug, quantity, cart_action, csrf, cart_url_fn)
parts = ['']
parts.append(add_html)
sp_val, rp_val = pr.get("sp_val"), pr.get("rp_val")
if sp_val:
parts.append(f'
Special price
')
parts.append(f'
{_price_str(sp_val, pr["sp_raw"], pr["sp_cur"])}
')
if rp_val:
parts.append(f'
{_price_str(rp_val, pr["rp_raw"], pr["rp_cur"])}
')
elif rp_val:
parts.append(f'
Our price
')
parts.append(f'
{_price_str(rp_val, pr["rp_raw"], pr["rp_cur"])}
')
# RRP
rrp_raw = d.get("rrp_raw")
rrp_val = d.get("rrp")
case_size = d.get("case_size_count") or 1
if rrp_raw and rrp_val:
rrp_str = f"{rrp_raw[0]}{rrp_val * case_size:.2f}"
parts.append(f'
rrp: {rrp_str}
')
parts.append("
")
return "".join(parts)
def _cart_add_html(slug: str, quantity: int, action: str, csrf: str,
cart_url_fn: Any = None) -> str:
"""Render add-to-cart button or quantity controls."""
if not quantity:
return (
f''
f'
'
)
cart_href = cart_url_fn("/") if callable(cart_url_fn) else "/"
return (
f''
)
# ---------------------------------------------------------------------------
# Mobile nav panel
# ---------------------------------------------------------------------------
def _mobile_nav_panel_html(ctx: dict) -> str:
"""Build mobile nav panel with category accordion."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
categories = ctx.get("categories", {})
qs = ctx.get("qs", "")
category_label = ctx.get("category_label", "")
top_slug = ctx.get("top_slug", "")
sub_slug = ctx.get("sub_slug", "")
hx_select = ctx.get("hx_select_search", "#main-panel")
select_colours = ctx.get("select_colours", "")
parts = ['']
all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
parts.append(
f'
'
f'All
'
)
for cat, data in categories.items():
cat_slug = data.get("slug", "")
cat_active = (top_slug == cat_slug.lower() if top_slug else False)
open_attr = " open" if cat_active else ""
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 ""
parts.append(f'
')
parts.append(
f''
f''
f'{escape(cat)}
'
f'{data.get("count", 0)}
'
f' '
f' '
)
subs = data.get("subs", [])
if subs:
parts.append('")
else:
view_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs
parts.append(
f''
)
parts.append(" ")
parts.append("
")
return "".join(parts)
# ---------------------------------------------------------------------------
# Product card (browse grid item)
# ---------------------------------------------------------------------------
def _product_card_html(p: dict, ctx: dict) -> str:
"""Render a single product card for browse grid."""
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()
cart_action = url_for("market.browse.product.cart", product_slug=slug)
# Like button overlay
like_html = ""
if user:
liked = p.get("is_liked", False)
like_html = _like_button_html(slug, liked, csrf, ctx)
# Image
image = p.get("image")
labels = p.get("labels", [])
brand = p.get("brand", "")
brand_highlight = " bg-yellow-200" if brand in selected_brands else ""
if image:
labels_html = "".join(
f' '
for l in labels
) if callable(asset_url_fn) else ""
img_html = (
f''
f'
'
f'
'
f'{labels_html}
'
f'{escape(brand)} '
f' '
)
else:
labels_list = "".join(f"{l} " for l in labels)
img_html = (
f''
f'
'
f'
No image
'
f'
'
f'
{escape(brand)}
'
f'
'
)
price_html = _card_price_html(p)
# Cart button
quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0
cart_url_fn = ctx.get("cart_url")
add_html = _cart_add_html(slug, quantity, cart_action, csrf, cart_url_fn)
# Stickers
stickers = p.get("stickers", [])
stickers_html = ""
if stickers and callable(asset_url_fn):
sticker_parts = []
for s in stickers:
found = s in selected_stickers
src = asset_url_fn(f"stickers/{s}.svg")
sticker_parts.append(
f' '
)
stickers_html = '' + "".join(sticker_parts) + "
"
# Title with search highlight
title = p.get("title", "")
if search and search.lower() in title.lower():
idx = title.lower().index(search.lower())
highlighted = f"{escape(title[:idx])}{escape(title[idx:idx+len(search)])} {escape(title[idx+len(search):])}"
else:
highlighted = escape(title)
return (
f''
)
def _like_button_html(slug: str, liked: bool, csrf: str, ctx: dict) -> str:
"""Render the like/unlike heart button overlay."""
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 (
f''
f'
'
)
# ---------------------------------------------------------------------------
# Product cards (pagination fragment)
# ---------------------------------------------------------------------------
def _product_cards_html(ctx: dict) -> str:
"""Render product cards with infinite scroll sentinels."""
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")
parts = [_product_card_html(p, ctx) for p in products]
if page < total_pages:
# Build next page URL
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
# Mobile sentinel
parts.append(
f''
f'
loading...
'
f'
Retrying...
'
)
# Desktop sentinel
parts.append(
f''
f'
loading...
'
f'
Retrying...
'
)
else:
parts.append('End of results
')
return "".join(parts)
# ---------------------------------------------------------------------------
# Browse filter panels (mobile + desktop)
# ---------------------------------------------------------------------------
def _desktop_filter_html(ctx: dict) -> str:
"""Build the desktop aside filter panel (search, category, sort, like, labels, stickers, brands)."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
category_label = ctx.get("category_label", "")
search = ctx.get("search", "")
search_count = ctx.get("search_count", "")
current_local_href = ctx.get("current_local_href", "/")
hx_select = ctx.get("hx_select", "#main-panel")
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", "")
asset_url_fn = ctx.get("asset_url")
# Search
search_html = search_desktop_html(ctx)
# Category summary + sort + like + labels + stickers
parts = [search_html]
parts.append(f'')
parts.append(f'
')
# Sort stickers
if sort_options:
parts.append(_sort_stickers_html(sort_options, sort, ctx))
# Like + labels row
parts.append('
')
parts.append(_like_filter_html(liked, liked_count, ctx))
if labels:
parts.append(_labels_filter_html(labels, selected_labels, ctx, prefix="nav-labels"))
parts.append(" ")
# Stickers
if stickers:
parts.append(_stickers_filter_html(stickers, selected_stickers, ctx))
# Subcategory selector
if subs_local and top_local_href:
parts.append(_subcategory_selector_html(subs_local, top_local_href, sub_slug, ctx))
parts.append("
")
# Brand filter
parts.append(f'')
if brands:
parts.append(_brand_filter_html(brands, selected_brands, ctx))
parts.append("
")
return "".join(parts)
def _mobile_filter_summary_html(ctx: dict) -> str:
"""Build mobile filter summary (collapsible bar showing active filters)."""
# Simplified version — just the filter details/summary wrapper
asset_url_fn = ctx.get("asset_url")
search = ctx.get("search", "")
search_count = ctx.get("search_count", "")
current_local_href = ctx.get("current_local_href", "/")
hx_select = ctx.get("hx_select", "#main-panel")
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 = search_mobile_html(ctx)
# Summary chips showing active filters
chip_parts = ['']
if sort and sort_options:
for k, l, i in sort_options:
if k == sort and callable(asset_url_fn):
chip_parts.append(f'
'
f' ')
if liked:
chip_parts.append('
'
f'
')
if liked_count is not None:
cls = "text-[10px] text-stone-500" if liked_count != 0 else "text-md text-red-500 font-bold"
chip_parts.append(f'
{liked_count}
')
chip_parts.append("
")
# Selected labels
if selected_labels:
chip_parts.append('
')
for sl in selected_labels:
for lb in labels:
if lb.get("name") == sl and callable(asset_url_fn):
chip_parts.append(f''
f' ')
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_parts.append(f'{lb["count"]}
')
chip_parts.append(" ")
chip_parts.append(" ")
# Selected stickers
if selected_stickers:
chip_parts.append('
')
for ss in selected_stickers:
for st in stickers:
if st.get("name") == ss and callable(asset_url_fn):
chip_parts.append(f''
f' ')
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_parts.append(f'{st["count"]}
')
chip_parts.append(" ")
chip_parts.append(" ")
# Selected brands
if selected_brands:
chip_parts.append('
')
for b in selected_brands:
count = 0
for br in brands:
if br.get("name") == b:
count = br.get("count", 0)
if count:
chip_parts.append(f'{escape(b)}
{count}
')
else:
chip_parts.append(f'{escape(b)}
0
')
chip_parts.append(" ")
chip_parts.append("
")
chips_html = "".join(chip_parts)
# Full mobile filter details
from shared.utils import route_prefix
prefix = route_prefix()
mobile_filter = _mobile_filter_content_html(ctx, prefix)
return (
f''
f''
f'{search_bar}'
f''
f'{chips_html}'
f'
'
f''
f'{mobile_filter}'
f'
'
)
def _mobile_filter_content_html(ctx: dict, prefix: str) -> str:
"""Build the expanded mobile filter panel contents."""
from shared.utils import route_prefix
search = ctx.get("search", "")
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", [])
asset_url_fn = ctx.get("asset_url")
qs_fn = ctx.get("qs_filter")
parts = []
# Sort options
if sort_options:
parts.append(_sort_stickers_html(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(
f''
)
# Like + labels row
parts.append('')
parts.append(_like_filter_html(liked, liked_count, ctx, mobile=True))
if labels:
parts.append(_labels_filter_html(labels, selected_labels, ctx, prefix="nav-labels", mobile=True))
parts.append("
")
# Stickers
if stickers:
parts.append(_stickers_filter_html(stickers, selected_stickers, ctx, mobile=True))
# Brands
if brands:
parts.append(_brand_filter_html(brands, selected_brands, ctx, mobile=True))
return "".join(parts)
def _sort_stickers_html(sort_options: list, current_sort: str, ctx: dict, mobile: bool = False) -> str:
"""Render sort option stickers."""
asset_url_fn = ctx.get("asset_url")
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
prefix = route_prefix()
parts = ['']
for k, label, icon in sort_options:
if callable(qs_fn):
href = prefix + current_local_href + qs_fn({"sort": k})
else:
href = "#"
active = (k == current_sort)
ring = " ring-2 ring-emerald-500 rounded" if active else ""
src = asset_url_fn(icon) if callable(asset_url_fn) else icon
parts.append(
f'
'
f' '
f'{escape(label)} '
)
parts.append("
")
return "".join(parts)
def _like_filter_html(liked: bool, liked_count: int, ctx: dict, mobile: bool = False) -> str:
"""Render the like filter toggle."""
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
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 (
f''
f' '
)
def _labels_filter_html(labels: list, selected: list, ctx: dict, *,
prefix: str = "nav-labels", mobile: bool = False) -> str:
"""Render label filter buttons."""
asset_url_fn = ctx.get("asset_url")
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()
parts = []
for lb in labels:
name = lb.get("name", "")
is_sel = name in selected
if callable(qs_fn):
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})
else:
href = "#"
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 ""
parts.append(
f''
f' '
)
return "".join(parts)
def _stickers_filter_html(stickers: list, selected: list, ctx: dict, mobile: bool = False) -> str:
"""Render sticker filter grid."""
asset_url_fn = ctx.get("asset_url")
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()
parts = ['']
for st in stickers:
name = st.get("name", "")
count = st.get("count", 0)
is_sel = name in selected
if callable(qs_fn):
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})
else:
href = "#"
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 ""
cls = "text-[10px] text-stone-500" if count != 0 else "text-md text-red-500 font-bold"
parts.append(
f'
'
f' '
f'{count} '
)
parts.append("
")
return "".join(parts)
def _brand_filter_html(brands: list, selected: list, ctx: dict, mobile: bool = False) -> str:
"""Render brand filter checkboxes."""
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()
parts = ['']
for br in brands:
name = br.get("name", "")
count = br.get("count", 0)
is_sel = name in selected
if callable(qs_fn):
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})
else:
href = "#"
bg = " bg-yellow-200" if is_sel else ""
cls = "text-md" if count else "text-md text-red-500"
parts.append(
f'
'
f'{escape(name)}
'
f'{count}
'
)
parts.append("
")
return "".join(parts)
def _subcategory_selector_html(subs: list, top_href: str, current_sub: str, ctx: dict) -> str:
"""Render subcategory vertical nav."""
hx_select = ctx.get("hx_select_search", "#main-panel")
from shared.utils import route_prefix
rp = route_prefix()
parts = ['']
# "All" link
parts.append(
f'
All '
)
for sub in subs:
slug = sub.get("slug", "")
name = sub.get("name", "")
href = sub.get("href", "")
active = (slug == current_sub)
parts.append(
f'
{escape(name)} '
)
parts.append("
")
return "".join(parts)
# ---------------------------------------------------------------------------
# Product detail page content
# ---------------------------------------------------------------------------
def _product_detail_html(d: dict, ctx: dict) -> str:
"""Build product detail main panel content."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
asset_url_fn = ctx.get("asset_url")
user = ctx.get("user")
liked_by_current_user = ctx.get("liked_by_current_user", False)
csrf = generate_csrf_token()
images = d.get("images", [])
labels = d.get("labels", [])
stickers = d.get("stickers", [])
brand = d.get("brand", "")
slug = d.get("slug", "")
# Gallery
if images:
# Like button
like_html = ""
if user:
like_html = _like_button_html(slug, liked_by_current_user, csrf, ctx)
# Main image + labels
labels_overlay = "".join(
f' '
for l in labels
) if callable(asset_url_fn) else ""
gallery_html = (
f''
f'{like_html}'
f'
'
f'
'
f'{labels_overlay}
'
f'{escape(brand)} '
)
# Prev/next buttons
if len(images) > 1:
gallery_html += (
'
‹ '
'
› '
)
gallery_html += "
"
# Thumbnails
if len(images) > 1:
thumbs = "".join(
f''
f' '
f' '
for i, u in enumerate(images)
)
gallery_html += f''
else:
like_html = ""
if user:
like_html = _like_button_html(slug, liked_by_current_user, csrf, ctx)
gallery_html = (
f''
f'{like_html}No image
'
)
# Stickers below gallery
stickers_html = ""
if stickers and callable(asset_url_fn):
sticker_parts = "".join(
f' '
for s in stickers
)
stickers_html = f'{sticker_parts}
'
# Right column: prices, description, sections
pr = _set_prices(d)
details_parts = ['']
# Unit price / case size extras
extras = []
ppu = d.get("price_per_unit") or d.get("price_per_unit_raw")
if ppu:
extras.append(f'
Unit price: {_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"):
extras.append(f'
Case size: {d["case_size_raw"]}
')
if extras:
details_parts.append('
' + "".join(extras) + "
")
# Description
desc_short = d.get("description_short")
desc_html = d.get("description_html")
if desc_short or desc_html:
details_parts.append('
')
if desc_short:
details_parts.append(f'
{escape(desc_short)}
')
if desc_html:
details_parts.append(f'
{desc_html}
')
details_parts.append("
")
# Sections (expandable)
sections = d.get("sections", [])
if sections:
details_parts.append('
')
for sec in sections:
details_parts.append(
f'
'
f''
f'{escape(sec.get("title", ""))} '
f'⌄ '
f'{sec.get("html", "")}
'
)
details_parts.append("
")
details_parts.append("
")
return (
f''
f'
{gallery_html}{stickers_html}
'
f'{"".join(details_parts)}
'
)
# ---------------------------------------------------------------------------
# Product meta (OpenGraph, JSON-LD)
# ---------------------------------------------------------------------------
def _product_meta_html(d: dict, ctx: dict) -> str:
"""Build product meta tags for ."""
import json
from quart import request
title = d.get("title", "")
desc_source = d.get("description_short") or ""
if not desc_source and d.get("description_html"):
# Strip HTML tags (simple approach)
import re
desc_source = re.sub(r"<[^>]+>", "", d.get("description_html", ""))
description = desc_source.strip().replace("\n", " ")[:160]
image_url = d.get("image") or (d.get("images", [None])[0] if d.get("images") else None)
canonical = request.url if request else ""
brand = d.get("brand", "")
sku = d.get("sku", "")
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")
parts = [f"{escape(title)} "]
parts.append(f' ')
if canonical:
parts.append(f' ')
# OpenGraph
site_title = ctx.get("base_title", "")
parts.append(f' ')
parts.append(' ')
parts.append(f' ')
parts.append(f' ')
if canonical:
parts.append(f' ')
if image_url:
parts.append(f' ')
if price and price_currency:
parts.append(f' ')
parts.append(f' ')
if brand:
parts.append(f' ')
# Twitter
card_type = "summary_large_image" if image_url else "summary"
parts.append(f' ')
parts.append(f' ')
parts.append(f' ')
if image_url:
parts.append(f' ')
# JSON-LD
jsonld = {
"@context": "https://schema.org",
"@type": "Product",
"name": title,
"image": image_url,
"description": description,
"sku": sku,
"url": canonical,
}
if brand:
jsonld["brand"] = {"@type": "Brand", "name": brand}
if price and price_currency:
jsonld["offers"] = {
"@type": "Offer",
"price": price,
"priceCurrency": price_currency,
"url": canonical,
"availability": "https://schema.org/InStock",
}
parts.append(f'')
return "\n".join(parts)
# ---------------------------------------------------------------------------
# Market cards (all markets / page markets)
# ---------------------------------------------------------------------------
def _market_card_html(market: Any, page_info: dict, *, show_page_badge: bool = True,
post_slug: str = "") -> str:
"""Render 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)
if show_page_badge and page_info:
pi = page_info.get(container_id, {})
p_slug = pi.get("slug", "")
p_title = pi.get("title", "")
market_href = market_url(f"/{p_slug}/{slug}/") if p_slug else ""
else:
p_slug = post_slug
p_title = ""
market_href = market_url(f"/{post_slug}/{slug}/") if post_slug else ""
parts = ['']
parts.append("")
if market_href:
parts.append(f'
{escape(name)} ')
else:
parts.append(f'
{escape(name)} ')
if description:
parts.append(f'
{escape(description)}
')
parts.append("
")
if show_page_badge and p_title:
badge_href = market_url(f"/{p_slug}/")
parts.append(
f''
)
parts.append(" ")
return "".join(parts)
def _market_cards_html(markets: list, page_info: dict, page: int, has_more: bool,
next_url: str, *, show_page_badge: bool = True,
post_slug: str = "") -> str:
"""Render market cards with infinite scroll sentinel."""
parts = [_market_card_html(m, page_info, show_page_badge=show_page_badge,
post_slug=post_slug) for m in markets]
if has_more:
parts.append(
f''
)
return "".join(parts)
# ---------------------------------------------------------------------------
# OOB header helpers
# ---------------------------------------------------------------------------
def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
"""Wrap a header row in OOB div with child placeholder."""
return (
f''
)
# ===========================================================================
# PUBLIC API
# ===========================================================================
# ---------------------------------------------------------------------------
# All markets
# ---------------------------------------------------------------------------
async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""Full page: all markets listing."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_html(markets, page_info, page, has_more, next_url)
content = f'{cards}
'
else:
content = (''
'
'
'
No markets available
')
content += '
'
hdr = root_header_html(ctx)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""OOB response: all markets listing."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_html(markets, page_info, page, has_more, next_url)
content = f'{cards}
'
else:
content = (''
'
'
'
No markets available
')
content += '
'
oobs = root_header_html(ctx, oob=True)
return oob_page(ctx, oobs_html=oobs, content_html=content)
async def render_all_markets_cards(markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""Pagination fragment: all markets cards."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
return _market_cards_html(markets, page_info, page, has_more, next_url)
# ---------------------------------------------------------------------------
# Page markets
# ---------------------------------------------------------------------------
async def render_page_markets_page(ctx: dict, markets: list, has_more: bool,
page: int) -> str:
"""Full page: page-scoped markets listing."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
post = ctx.get("post", {})
post_slug = post.get("slug", "")
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_html(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = f'{cards}
'
else:
content = (''
'
'
'
No markets for this page
')
content += '
'
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "w-full" (raw! ph))',
ph=_post_header_html(ctx),
)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool,
page: int) -> str:
"""OOB response: page-scoped markets."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
post = ctx.get("post", {})
post_slug = post.get("slug", "")
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_html(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = f'{cards}
'
else:
content = (''
'
'
'
No markets for this page
')
content += '
'
oobs = _oob_header_html("post-header-child", "market-header-child", "")
oobs += _post_header_html(ctx, oob=True)
return oob_page(ctx, oobs_html=oobs, content_html=content)
async def render_page_markets_cards(markets: list, has_more: bool,
page: int, post_slug: str) -> str:
"""Pagination fragment: page-scoped markets cards."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
return _market_cards_html(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
# ---------------------------------------------------------------------------
# Market landing page
# ---------------------------------------------------------------------------
async def render_market_home_page(ctx: dict) -> str:
"""Full page: market landing page (post content)."""
post = ctx.get("post") or {}
content = _market_landing_content(post)
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "w-full" (raw! ph (raw! mh)))',
ph=_post_header_html(ctx),
mh=_market_header_html(ctx),
)
menu = _mobile_nav_panel_html(ctx)
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=menu)
async def render_market_home_oob(ctx: dict) -> str:
"""OOB response: market landing page."""
post = ctx.get("post") or {}
content = _market_landing_content(post)
oobs = _oob_header_html("post-header-child", "market-header-child",
_market_header_html(ctx))
oobs += _post_header_html(ctx, oob=True)
menu = _mobile_nav_panel_html(ctx)
return oob_page(ctx, oobs_html=oobs, content_html=content, menu_html=menu)
def _market_landing_content(post: dict) -> str:
"""Build market landing page content (excerpt + feature image + html)."""
parts = ['']
if post.get("custom_excerpt"):
parts.append(f'{post["custom_excerpt"]}
')
if post.get("feature_image"):
parts.append(
f''
f'
'
)
if post.get("html"):
parts.append(f'{post["html"]}
')
parts.append('
')
return "".join(parts)
# ---------------------------------------------------------------------------
# Browse page
# ---------------------------------------------------------------------------
async def render_browse_page(ctx: dict) -> str:
"""Full page: product browse with filters."""
cards_html = _product_cards_html(ctx)
content = f'{cards_html}
'
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "w-full" (raw! ph (raw! mh)))',
ph=_post_header_html(ctx),
mh=_market_header_html(ctx),
)
menu = _mobile_nav_panel_html(ctx)
filter_html = _mobile_filter_summary_html(ctx)
aside_html = _desktop_filter_html(ctx)
return full_page(ctx, header_rows_html=hdr, content_html=content,
menu_html=menu, filter_html=filter_html, aside_html=aside_html)
async def render_browse_oob(ctx: dict) -> str:
"""OOB response: product browse."""
cards_html = _product_cards_html(ctx)
content = f'{cards_html}
'
oobs = _oob_header_html("post-header-child", "market-header-child",
_market_header_html(ctx))
oobs += _post_header_html(ctx, oob=True)
menu = _mobile_nav_panel_html(ctx)
filter_html = _mobile_filter_summary_html(ctx)
aside_html = _desktop_filter_html(ctx)
return oob_page(ctx, oobs_html=oobs, content_html=content,
menu_html=menu, filter_html=filter_html, aside_html=aside_html)
async def render_browse_cards(ctx: dict) -> str:
"""Pagination fragment: product cards only."""
return _product_cards_html(ctx)
# ---------------------------------------------------------------------------
# Product detail
# ---------------------------------------------------------------------------
async def render_product_page(ctx: dict, d: dict) -> str:
"""Full page: product detail."""
content = _product_detail_html(d, ctx)
meta = _product_meta_html(d, ctx)
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "w-full" (raw! ph (raw! mh (raw! prh))))',
ph=_post_header_html(ctx),
mh=_market_header_html(ctx),
prh=_product_header_html(ctx, d),
)
return full_page(ctx, header_rows_html=hdr, content_html=content, meta_html=meta)
async def render_product_oob(ctx: dict, d: dict) -> str:
"""OOB response: product detail."""
content = _product_detail_html(d, ctx)
oobs = _market_header_html(ctx, oob=True)
oobs += _oob_header_html("market-header-child", "product-header-child",
_product_header_html(ctx, d))
menu = _mobile_nav_panel_html(ctx)
return oob_page(ctx, oobs_html=oobs, content_html=content, menu_html=menu)
# ---------------------------------------------------------------------------
# Product admin
# ---------------------------------------------------------------------------
async def render_product_admin_page(ctx: dict, d: dict) -> str:
"""Full page: product admin."""
content = _product_detail_html(d, ctx)
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "w-full" (raw! ph (raw! mh (raw! prh (raw! pah)))))',
ph=_post_header_html(ctx),
mh=_market_header_html(ctx),
prh=_product_header_html(ctx, d),
pah=_product_admin_header_html(ctx, d),
)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_product_admin_oob(ctx: dict, d: dict) -> str:
"""OOB response: product admin."""
content = _product_detail_html(d, ctx)
oobs = _product_header_html(ctx, d, oob=True)
oobs += _oob_header_html("product-header-child", "product-admin-header-child",
_product_admin_header_html(ctx, d))
return oob_page(ctx, oobs_html=oobs, content_html=content)
def _product_admin_header_html(ctx: dict, d: dict, *, oob: bool = False) -> str:
"""Build product admin header row."""
from quart import url_for
slug = d.get("slug", "")
link_href = url_for("market.browse.product.admin", product_slug=slug)
return sexp(
'(~menu-row :id "product-admin-row" :level 4'
' :link-href lh :link-label "admin!!" :icon "fa fa-cog"'
' :child-id "product-admin-header-child" :oob oob)',
lh=link_href,
oob=oob,
)
# ---------------------------------------------------------------------------
# Market admin
# ---------------------------------------------------------------------------
async def render_market_admin_page(ctx: dict) -> str:
"""Full page: market admin."""
content = "market admin"
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "w-full" (raw! ph (raw! mh (raw! mah))))',
ph=_post_header_html(ctx),
mh=_market_header_html(ctx),
mah=_market_admin_header_html(ctx),
)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_market_admin_oob(ctx: dict) -> str:
"""OOB response: market admin."""
content = "market admin"
oobs = _market_header_html(ctx, oob=True)
oobs += _oob_header_html("market-header-child", "market-admin-header-child",
_market_admin_header_html(ctx))
return oob_page(ctx, oobs_html=oobs, content_html=content)
def _market_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build market admin header row."""
from quart import url_for
link_href = url_for("market.admin.admin")
return sexp(
'(~menu-row :id "market-admin-row" :level 3'
' :link-href lh :link-label "admin" :icon "fa fa-cog"'
' :child-id "market-admin-header-child" :oob oob)',
lh=link_href,
oob=oob,
)