"""Price helpers, OOB helpers, product detail/meta builders.""" from __future__ import annotations from typing import Any from shared.sx.parser import SxExpr from shared.sx.helpers import sx_call # --------------------------------------------------------------------------- # OOB orphan cleanup # --------------------------------------------------------------------------- _MARKET_DEEP_IDS = [ "product-admin-row", "product-admin-header-child", "product-row", "product-header-child", "market-admin-row", "market-admin-header-child", "market-row", "market-header-child", "post-admin-row", "post-admin-header-child", ] def _clear_deeper_oob(*keep_ids: str) -> str: """Clear all market header rows/children NOT in keep_ids.""" to_clear = [i for i in _MARKET_DEEP_IDS if i not in keep_ids] return " ".join(sx_call("clear-oob-div", id=i) for i in to_clear) # --------------------------------------------------------------------------- # Price helpers # --------------------------------------------------------------------------- _SYM = {"GBP": "\u00a3", "EUR": "\u20ac", "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_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 # --------------------------------------------------------------------------- def _product_detail_sx(d: dict, ctx: dict) -> str: """Build product detail main panel content as sx.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token from .cards import _like_button_sx 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_sx = "" if user: like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx) # Main image + labels label_parts: list[str] = [] if callable(asset_url_fn): 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( "market-detail-gallery-inner", 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 nav_buttons = "" if len(images) > 1: nav_buttons = sx_call("market-detail-nav-buttons") gallery_sx = sx_call( "market-detail-gallery", 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): sticker_parts = [] for s in stickers: sticker_parts.append(sx_call( "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 pr = _set_prices(d) 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") if ppu: extra_parts.append(sx_call( "market-detail-unit-price", 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"): extra_parts.append(sx_call("market-detail-case-size", size=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 desc_short = d.get("description_short") desc_html_val = d.get("description_html") if desc_short or desc_html_val: desc_parts: list[str] = [] if desc_short: desc_parts.append(sx_call("market-detail-desc-short", text=desc_short)) if desc_html_val: desc_parts.append(sx_call("market-detail-desc-html", html=desc_html_val)) desc_inner = "(<> " + " ".join(desc_parts) + ")" detail_parts.append(sx_call("market-detail-desc-wrapper", inner=SxExpr(desc_inner))) # 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 "(<>)" details_sx = sx_call("market-detail-right-col", inner=SxExpr(details_inner_sx)) return sx_call( "market-detail-layout", gallery=SxExpr(gallery_final), stickers=stickers_sx or None, details=details_sx, ) # --------------------------------------------------------------------------- # Product meta (OpenGraph, JSON-LD) # --------------------------------------------------------------------------- def _product_meta_sx(d: dict, ctx: dict) -> str: """Build product meta tags as sx (auto-hoisted to by sx.js).""" 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"): 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 = [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 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(sx_call("market-meta-jsonld", json=json.dumps(jsonld))) return "(<> " + " ".join(parts) + ")"