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>
158 lines
8.2 KiB
Plaintext
158 lines
8.2 KiB
Plaintext
;; Market card components — pure data, no raw! HTML injection
|
|
|
|
(defcomp ~market-label-overlay (&key src)
|
|
(img :src src :alt ""
|
|
:class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top"))
|
|
|
|
(defcomp ~market-card-image (&key image labels brand brand-highlight)
|
|
(div :class "w-full aspect-square bg-stone-100 relative"
|
|
(figure :class "inline-block w-full h-full"
|
|
(div :class "relative w-full h-full"
|
|
(img :src image :alt "no image" :class "absolute inset-0 w-full h-full object-contain object-top" :loading "lazy" :decoding "async" :fetchpriority "low")
|
|
(when labels (map (lambda (src) (~market-label-overlay :src src)) labels)))
|
|
(figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand))))
|
|
|
|
(defcomp ~market-card-no-image (&key labels brand)
|
|
(div :class "w-full aspect-square bg-stone-100 relative"
|
|
(div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative"
|
|
(div :class "text-stone-400 text-xs" "No image")
|
|
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
|
|
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
|
|
|
|
(defcomp ~market-card-sticker (&key src name ring-cls)
|
|
(img :src src :alt name :class (str "w-6 h-6" ring-cls)))
|
|
|
|
(defcomp ~market-card-stickers (&key stickers)
|
|
(div :class "flex flex-row justify-center gap-2 p-2"
|
|
(map (lambda (s) (~market-card-sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers)))
|
|
|
|
(defcomp ~market-card-highlight (&key pre mid post)
|
|
(<> pre (mark mid) post))
|
|
|
|
;; Price — delegates to shared ~price
|
|
(defcomp ~market-card-price (&key special-price regular-price)
|
|
(~price :special-price special-price :regular-price regular-price))
|
|
|
|
;; Main product card — accepts pure data, composes sub-components
|
|
(defcomp ~market-product-card (&key href hx-select
|
|
has-like liked slug csrf like-action
|
|
image labels brand brand-highlight
|
|
special-price regular-price
|
|
cart-action quantity cart-href
|
|
stickers
|
|
title has-highlight search-pre search-mid search-post)
|
|
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
|
|
(when has-like
|
|
(~market-like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf
|
|
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")))
|
|
(a :href href :sx-get href :sx-target "#main-panel"
|
|
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
|
|
(if image
|
|
(~market-card-image :image image :labels labels :brand brand :brand-highlight brand-highlight)
|
|
(~market-card-no-image :labels labels :brand brand))
|
|
(~market-card-price :special-price special-price :regular-price regular-price))
|
|
(div :class "flex justify-center"
|
|
(if quantity
|
|
(~market-cart-add-quantity :cart-id (str "cart-" slug) :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 (str "cart-" slug) :action cart-action :csrf csrf)))
|
|
(a :href href :sx-get href :sx-target "#main-panel"
|
|
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
|
|
(when stickers (~market-card-stickers :stickers stickers))
|
|
(div :class "text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]"
|
|
(if has-highlight
|
|
(~market-card-highlight :pre search-pre :mid search-mid :post search-post)
|
|
title)))))
|
|
|
|
(defcomp ~market-like-button (&key form-id action slug csrf icon-cls)
|
|
(div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl"
|
|
(form :id form-id :action action :method "post"
|
|
:sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML"
|
|
(input :type "hidden" :name "csrf_token" :value csrf)
|
|
(button :type "submit" :class "cursor-pointer"
|
|
(i :class icon-cls :aria-hidden "true")))))
|
|
|
|
(defcomp ~market-market-card-title-link (&key href name)
|
|
(a :href href :class "hover:text-emerald-700"
|
|
(h2 :class "text-lg font-semibold text-stone-900" name)))
|
|
|
|
(defcomp ~market-market-card-title (&key name)
|
|
(h2 :class "text-lg font-semibold text-stone-900" name))
|
|
|
|
(defcomp ~market-market-card-desc (&key description)
|
|
(p :class "text-sm text-stone-600 mt-1 line-clamp-2" description))
|
|
|
|
(defcomp ~market-market-card-badge (&key href title)
|
|
(div :class "flex flex-wrap items-center gap-1.5 mt-3"
|
|
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200"
|
|
title)))
|
|
|
|
(defcomp ~market-market-card (&key title-content desc-content badge-content title desc badge)
|
|
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors"
|
|
(div
|
|
(if title-content title-content (when title title))
|
|
(if desc-content desc-content (when desc desc)))
|
|
(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)))))
|
|
|