Externalize sexp to .sexpr files + render() API
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
Replace all 676 inline sexp() string calls across 7 services with render(component_name, **kwargs) calls backed by 46 external .sexpr component definition files (587 defcomps total). - Add render() function to shared/sexp/jinja_bridge.py - Add load_service_components() helper and update load_sexp_dir() for *.sexpr - Update parser keyword regex to support HTMX hx-on::event syntax - Convert remaining inline HTML in route files to render() calls - Add shared/sexp/templates/misc.sexp for cross-service utility components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ def register():
|
||||
async def _container_nav_handler():
|
||||
from quart import current_app
|
||||
from shared.infrastructure.urls import market_url
|
||||
from shared.sexp.jinja_bridge import sexp as render_sexp
|
||||
from shared.sexp.jinja_bridge import render as render_comp
|
||||
|
||||
container_type = request.args.get("container_type", "page")
|
||||
container_id = int(request.args.get("container_id", 0))
|
||||
@@ -51,9 +51,9 @@ def register():
|
||||
parts = []
|
||||
for m in markets:
|
||||
href = market_url(f"/{post_slug}/{m.slug}/")
|
||||
parts.append(render_sexp(
|
||||
'(~market-link-nav :href href :name name :nav-class nav-class)',
|
||||
href=href, name=m.name, **{"nav-class": nav_class},
|
||||
parts.append(render_comp(
|
||||
"market-link-nav",
|
||||
href=href, name=m.name, nav_class=nav_class,
|
||||
))
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -65,7 +65,7 @@ def register():
|
||||
from sqlalchemy import select
|
||||
from shared.models.market import Product
|
||||
from shared.infrastructure.urls import market_url
|
||||
from shared.sexp.jinja_bridge import sexp as render_sexp
|
||||
from shared.sexp.jinja_bridge import render as render_comp
|
||||
|
||||
slug = request.args.get("slug", "")
|
||||
keys_raw = request.args.get("keys", "")
|
||||
@@ -86,8 +86,8 @@ def register():
|
||||
detail = f"<s>{product.regular_price}</s> {product.special_price}"
|
||||
elif product.regular_price:
|
||||
detail = str(product.regular_price)
|
||||
parts.append(render_sexp(
|
||||
'(~link-card :title title :image image :subtitle subtitle :detail detail :link link)',
|
||||
parts.append(render_comp(
|
||||
"link-card",
|
||||
title=product.title, image=product.image,
|
||||
subtitle=subtitle, detail=detail,
|
||||
link=market_url(f"/product/{product.slug}/"),
|
||||
@@ -108,8 +108,8 @@ def register():
|
||||
detail = f"<s>{product.regular_price}</s> {product.special_price}"
|
||||
elif product.regular_price:
|
||||
detail = str(product.regular_price)
|
||||
return render_sexp(
|
||||
'(~link-card :title title :image image :subtitle subtitle :detail detail :link link)',
|
||||
return render_comp(
|
||||
"link-card",
|
||||
title=product.title, image=product.image,
|
||||
subtitle=subtitle, detail=detail,
|
||||
link=market_url(f"/product/{product.slug}/"),
|
||||
|
||||
105
market/sexp/cards.sexpr
Normal file
105
market/sexp/cards.sexpr
Normal file
@@ -0,0 +1,105 @@
|
||||
;; Market card components
|
||||
|
||||
(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-html brand-highlight brand)
|
||||
(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")
|
||||
(raw! labels-html))
|
||||
(figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand))))
|
||||
|
||||
(defcomp ~market-card-no-image (&key labels-html 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")
|
||||
(ul :class "flex flex-row gap-1" (raw! labels-html))
|
||||
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
|
||||
|
||||
(defcomp ~market-card-label-item (&key label)
|
||||
(li label))
|
||||
|
||||
(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 items-html)
|
||||
(div :class "flex flex-row justify-center gap-2 p-2" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-card-highlight (&key pre mid post)
|
||||
(<> pre (mark mid) post))
|
||||
|
||||
(defcomp ~market-card-text (&key text)
|
||||
(<> text))
|
||||
|
||||
(defcomp ~market-product-card (&key like-html href hx-select image-html price-html add-html stickers-html title-html)
|
||||
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
|
||||
(raw! like-html)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
(raw! image-html) (raw! price-html))
|
||||
(div :class "flex justify-center" (raw! add-html))
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
(raw! stickers-html)
|
||||
(div :class "text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]"
|
||||
(raw! title-html)))))
|
||||
|
||||
(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"
|
||||
:hx-post action :hx-target (str "#like-" slug) :hx-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-html desc-html badge-html)
|
||||
(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 (raw! title-html) (raw! desc-html))
|
||||
(raw! badge-html)))
|
||||
|
||||
(defcomp ~market-sentinel-mobile (&key id next-url hyperscript)
|
||||
(div :id id
|
||||
:class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
|
||||
:hx-get next-url :hx-trigger "intersect once delay:250ms, sentinelmobile:retry"
|
||||
:hx-swap "outerHTML"
|
||||
:_ hyperscript
|
||||
:role "status" :aria-live "polite" :aria-hidden "true"
|
||||
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
|
||||
(div :class "js-neterr hidden text-center text-xs text-stone-400" "Retrying...")))
|
||||
|
||||
(defcomp ~market-sentinel-desktop (&key id next-url hyperscript)
|
||||
(div :id id
|
||||
:class "hidden md:block h-4 opacity-0 pointer-events-none"
|
||||
:hx-get next-url :hx-trigger "intersect once delay:250ms, sentinel:retry"
|
||||
:hx-swap "outerHTML"
|
||||
:_ hyperscript
|
||||
:role "status" :aria-live "polite" :aria-hidden "true"
|
||||
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
|
||||
(div :class "js-neterr hidden text-center text-xs text-stone-400" "Retrying...")))
|
||||
|
||||
(defcomp ~market-sentinel-end ()
|
||||
(div :class "col-span-full mt-4 text-center text-xs text-stone-400" "End of results"))
|
||||
|
||||
(defcomp ~market-market-sentinel (&key id next-url)
|
||||
(div :id id :class "h-4 opacity-0 pointer-events-none"
|
||||
:hx-get next-url :hx-trigger "intersect once delay:250ms"
|
||||
:hx-swap "outerHTML" :role "status" :aria-hidden "true"
|
||||
(div :class "text-center text-xs text-stone-400" "loading...")))
|
||||
44
market/sexp/cart.sexpr
Normal file
44
market/sexp/cart.sexpr
Normal file
@@ -0,0 +1,44 @@
|
||||
;; Market cart components
|
||||
|
||||
(defcomp ~market-cart-add-empty (&key cart-id action csrf)
|
||||
(div :id cart-id
|
||||
(form :action action :method "post" :hx-post action :hx-target "#cart-mini" :hx-swap "outerHTML" :class "rounded flex items-center"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "count" :value "1")
|
||||
(button :type "submit" :class "relative inline-flex items-center justify-center text-sm font-medium text-stone-500 hover:bg-emerald-50"
|
||||
(span :class "relative inline-flex items-center justify-center"
|
||||
(i :class "fa fa-cart-plus text-4xl" :aria-hidden "true"))))))
|
||||
|
||||
(defcomp ~market-cart-add-quantity (&key cart-id action csrf minus-val plus-val quantity cart-href)
|
||||
(div :id cart-id
|
||||
(div :class "rounded flex items-center gap-2"
|
||||
(form :action action :method "post" :hx-post action :hx-target "#cart-mini" :hx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "count" :value minus-val)
|
||||
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "-"))
|
||||
(a :class "relative inline-flex items-center justify-center text-emerald-700" :href cart-href
|
||||
(span :class "relative inline-flex items-center justify-center"
|
||||
(i :class "fa-solid fa-shopping-cart text-2xl" :aria-hidden "true")
|
||||
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||
(span :class "flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold" quantity))))
|
||||
(form :action action :method "post" :hx-post action :hx-target "#cart-mini" :hx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "count" :value plus-val)
|
||||
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+")))))
|
||||
|
||||
(defcomp ~market-cart-mini-count (&key href count)
|
||||
(div :id "cart-mini" :hx-swap-oob "outerHTML"
|
||||
(a :href href :class "relative inline-flex items-center justify-center"
|
||||
(span :class "relative inline-flex items-center justify-center"
|
||||
(i :class "fa-solid fa-shopping-cart text-xl" :aria-hidden "true")
|
||||
(span :class "absolute -top-1.5 -right-2 pointer-events-none"
|
||||
(span :class "flex items-center justify-center bg-emerald-500 text-white rounded-full min-w-[1.25rem] h-5 text-xs font-bold px-1"
|
||||
count))))))
|
||||
|
||||
(defcomp ~market-cart-mini-empty (&key href logo)
|
||||
(div :id "cart-mini" :hx-swap-oob "outerHTML"
|
||||
(a :href href :class "relative inline-flex items-center justify-center"
|
||||
(img :src logo :class "h-8 w-8 rounded-full object-cover border border-stone-300" :alt ""))))
|
||||
|
||||
(defcomp ~market-cart-add-oob (&key id inner-html)
|
||||
(div :id id :hx-swap-oob "outerHTML" (raw! inner-html)))
|
||||
94
market/sexp/detail.sexpr
Normal file
94
market/sexp/detail.sexpr
Normal file
@@ -0,0 +1,94 @@
|
||||
;; Market product detail components
|
||||
|
||||
(defcomp ~market-detail-gallery-inner (&key like-html image alt labels-html brand)
|
||||
(<> (raw! like-html)
|
||||
(figure :class "inline-block"
|
||||
(div :class "relative w-full aspect-square"
|
||||
(img :data-main-img "" :src image :alt alt
|
||||
:class "w-full h-full object-contain object-top" :loading "eager" :decoding "async")
|
||||
(raw! labels-html))
|
||||
(figcaption :class "mt-2 text-sm text-stone-600 text-center" brand))))
|
||||
|
||||
(defcomp ~market-detail-nav-buttons ()
|
||||
(<>
|
||||
(button :type "button" :data-prev ""
|
||||
:class "absolute left-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
|
||||
:title "Previous" "\u2039")
|
||||
(button :type "button" :data-next ""
|
||||
:class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
|
||||
:title "Next" "\u203a")))
|
||||
|
||||
(defcomp ~market-detail-gallery (&key inner-html nav-html)
|
||||
(div :class "relative rounded-xl overflow-hidden bg-stone-100"
|
||||
(raw! inner-html) (raw! nav-html)))
|
||||
|
||||
(defcomp ~market-detail-thumb (&key title src alt)
|
||||
(<> (button :type "button" :data-thumb ""
|
||||
:class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2"
|
||||
:title title
|
||||
(img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async"))
|
||||
(span :data-image-src src :class "hidden")))
|
||||
|
||||
(defcomp ~market-detail-thumbs (&key thumbs-html)
|
||||
(div :class "flex flex-row justify-center"
|
||||
(div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" (raw! thumbs-html))))
|
||||
|
||||
(defcomp ~market-detail-no-image (&key like-html)
|
||||
(div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400"
|
||||
(raw! like-html) "No image"))
|
||||
|
||||
(defcomp ~market-detail-sticker (&key src name)
|
||||
(img :src src :alt name :class "w-10 h-10"))
|
||||
|
||||
(defcomp ~market-detail-stickers (&key items-html)
|
||||
(div :class "p-2 flex flex-row justify-center gap-2" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-detail-unit-price (&key price)
|
||||
(div (str "Unit price: " price)))
|
||||
|
||||
(defcomp ~market-detail-case-size (&key size)
|
||||
(div (str "Case size: " size)))
|
||||
|
||||
(defcomp ~market-detail-extras (&key inner-html)
|
||||
(div :class "mt-2 space-y-1 text-sm text-stone-600" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-detail-desc-short (&key text)
|
||||
(p :class "leading-relaxed text-lg" text))
|
||||
|
||||
(defcomp ~market-detail-desc-html (&key html)
|
||||
(div :class "max-w-none text-sm leading-relaxed" (raw! html)))
|
||||
|
||||
(defcomp ~market-detail-desc-wrapper (&key inner-html)
|
||||
(div :class "mt-4 text-stone-800 space-y-3" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-detail-section (&key title html)
|
||||
(details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0"
|
||||
(summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between"
|
||||
(span :class "font-medium" title)
|
||||
(span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304"))
|
||||
(div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (raw! html))))
|
||||
|
||||
(defcomp ~market-detail-sections (&key items-html)
|
||||
(div :class "mt-8 space-y-3" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-detail-right-col (&key inner-html)
|
||||
(div :class "md:col-span-3" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-detail-layout (&key gallery-html stickers-html details-html)
|
||||
(<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root ""
|
||||
(div :class "md:col-span-2" (raw! gallery-html) (raw! stickers-html))
|
||||
(raw! details-html))
|
||||
(div :class "pb-8")))
|
||||
|
||||
(defcomp ~market-landing-excerpt (&key text)
|
||||
(div :class "w-full text-center italic text-3xl p-2" text))
|
||||
|
||||
(defcomp ~market-landing-image (&key src)
|
||||
(div :class "mb-3 flex justify-center"
|
||||
(img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover")))
|
||||
|
||||
(defcomp ~market-landing-html (&key html)
|
||||
(div :class "blog-content p-2" (raw! html)))
|
||||
|
||||
(defcomp ~market-landing-content (&key inner-html)
|
||||
(<> (article :class "relative w-full" (raw! inner-html)) (div :class "pb-8")))
|
||||
120
market/sexp/filters.sexpr
Normal file
120
market/sexp/filters.sexpr
Normal file
@@ -0,0 +1,120 @@
|
||||
;; Market filter components
|
||||
|
||||
(defcomp ~market-filter-sort-item (&key href hx-select ring-cls src label)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
|
||||
(img :src src :alt label :class "w-10 h-10")
|
||||
(span :class "text-xs" label)))
|
||||
|
||||
(defcomp ~market-filter-sort-row (&key items-html)
|
||||
(div :class "flex flex-row gap-2 justify-center p-1" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-filter-like (&key href hx-select icon-cls size-cls)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class "flex flex-col items-center gap-1 p-1 cursor-pointer"
|
||||
(i :aria-hidden "true" :class (str icon-cls " " size-cls " leading-none"))))
|
||||
|
||||
(defcomp ~market-filter-label-item (&key href hx-select ring-cls src name)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
|
||||
(img :src src :alt name :class "w-10 h-10")))
|
||||
|
||||
(defcomp ~market-filter-sticker-item (&key href hx-select ring-cls src name count-cls count)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
|
||||
(img :src src :alt name :class "w-6 h-6")
|
||||
(span :class count-cls count)))
|
||||
|
||||
(defcomp ~market-filter-stickers-row (&key items-html)
|
||||
(div :class "flex flex-wrap gap-2 justify-center p-1" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-filter-brand-item (&key href hx-select bg-cls name-cls name count)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class (str "flex flex-row items-center gap-2 px-2 py-1 rounded hover:bg-stone-100" bg-cls)
|
||||
(div :class name-cls name) (div :class name-cls count)))
|
||||
|
||||
(defcomp ~market-filter-brands-panel (&key items-html)
|
||||
(div :class "space-y-1 p-2" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-filter-category-label (&key label)
|
||||
(div :class "mb-4" (div :class "text-2xl uppercase tracking-wide text-black-500" label)))
|
||||
|
||||
(defcomp ~market-filter-like-labels-nav (&key inner-html)
|
||||
(nav :aria-label "like" :class "flex flex-row justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1"
|
||||
(raw! inner-html)))
|
||||
|
||||
(defcomp ~market-desktop-category-summary (&key inner-html)
|
||||
(div :id "category-summary-desktop" :hxx-swap-oob "outerHTML" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-desktop-brand-summary (&key inner-html)
|
||||
(div :id "filter-summary-desktop" :hxx-swap-oob "outerHTML" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-filter-subcategory-item (&key href hx-select active-cls name)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class (str "block px-2 py-1 rounded hover:bg-stone-100" active-cls)
|
||||
name))
|
||||
|
||||
(defcomp ~market-filter-subcategory-panel (&key items-html)
|
||||
(div :class "mt-4 space-y-1" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-mobile-clear-filters (&key href hx-select)
|
||||
(div :class "flex flex-row justify-center"
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:role "button" :title "clear filters" :aria-label "clear filters"
|
||||
:class "flex flex-col items-center justify-start p-1 rounded bg-stone-200 text-black cursor-pointer"
|
||||
(span :class "mt-1 leading-none tabular-nums" "clear filters"))))
|
||||
|
||||
(defcomp ~market-mobile-like-labels-row (&key inner-html)
|
||||
(div :class "flex flex-row gap-2 justify-center items-center" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-mobile-filter-summary (&key search-bar chips-html filter-html)
|
||||
(details :class "md:hidden group" :id "/filter"
|
||||
(summary :class "cursor-pointer select-none" :id "filter-summary-mobile"
|
||||
(raw! search-bar)
|
||||
(div :class "col-span-12 min-w-0 grid grid-cols-1 gap-1 bg-gray-100 px-2" :role "list"
|
||||
(raw! chips-html)))
|
||||
(div :id "filter-details-mobile" :style "display:contents"
|
||||
(raw! filter-html))))
|
||||
|
||||
(defcomp ~market-mobile-chips-row (&key inner-html)
|
||||
(div :class "flex flex-row items-start gap-2" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-mobile-chip-sort (&key src label)
|
||||
(ul :class "relative inline-flex items-center justify-center gap-2"
|
||||
(li :role "listitem" (img :src src :alt label :class "w-10 h-10"))))
|
||||
|
||||
(defcomp ~market-mobile-chip-liked-icon ()
|
||||
(i :aria-hidden "true" :class "fa-solid fa-heart text-red-500 text-[40px] leading-none"))
|
||||
|
||||
(defcomp ~market-mobile-chip-count (&key cls count)
|
||||
(div :class (str cls " mt-1 leading-none tabular-nums") count))
|
||||
|
||||
(defcomp ~market-mobile-chip-liked (&key inner-html)
|
||||
(div :class "flex flex-col items-center gap-1 pb-1" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-mobile-chip-image (&key src name)
|
||||
(img :src src :alt name :class "w-10 h-10"))
|
||||
|
||||
(defcomp ~market-mobile-chip-item (&key inner-html)
|
||||
(li :role "listitem" :class "flex flex-col items-center gap-1 pb-1" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-mobile-chip-list (&key items-html)
|
||||
(ul :class "relative inline-flex items-center justify-center gap-2" (raw! items-html)))
|
||||
|
||||
(defcomp ~market-mobile-chip-brand (&key name count)
|
||||
(li :role "listitem" :class "flex flex-row items-center gap-2"
|
||||
(div :class "text-md" name) (div :class "text-md" count)))
|
||||
|
||||
(defcomp ~market-mobile-chip-brand-zero (&key name)
|
||||
(li :role "listitem" :class "flex flex-row items-center gap-2"
|
||||
(div :class "text-md text-red-500" name) (div :class "text-xl text-red-500" "0")))
|
||||
|
||||
(defcomp ~market-mobile-chip-brand-list (&key items-html)
|
||||
(ul (raw! items-html)))
|
||||
22
market/sexp/grids.sexpr
Normal file
22
market/sexp/grids.sexpr
Normal file
@@ -0,0 +1,22 @@
|
||||
;; Market grid and layout components
|
||||
|
||||
(defcomp ~market-markets-grid (&key cards-html)
|
||||
(div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" (raw! cards-html)))
|
||||
|
||||
(defcomp ~market-no-markets (&key message)
|
||||
(div :class "px-3 py-12 text-center text-stone-400"
|
||||
(i :class "fa fa-store text-4xl mb-3" :aria-hidden "true")
|
||||
(p :class "text-lg" message)))
|
||||
|
||||
(defcomp ~market-product-grid (&key cards-html)
|
||||
(<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" (raw! cards-html)) (div :class "pb-8")))
|
||||
|
||||
(defcomp ~market-bottom-spacer ()
|
||||
(div :class "pb-8"))
|
||||
|
||||
(defcomp ~market-like-toggle-button (&key colour action hx-headers label icon-cls)
|
||||
(button :class (str "flex items-center gap-1 " colour " hover:text-red-600 transition-colors w-[1em] h-[1em]")
|
||||
:hx-post action :hx-target "this" :hx-swap "outerHTML" :hx-push-url "false"
|
||||
:hx-headers hx-headers
|
||||
:hx-swap-settle "0ms" :aria-label label
|
||||
(i :aria-hidden "true" :class icon-cls)))
|
||||
38
market/sexp/headers.sexpr
Normal file
38
market/sexp/headers.sexpr
Normal file
@@ -0,0 +1,38 @@
|
||||
;; Market header components
|
||||
|
||||
(defcomp ~market-post-label-image (&key src)
|
||||
(img :src src :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
|
||||
|
||||
(defcomp ~market-post-label-title (&key title)
|
||||
(span title))
|
||||
|
||||
(defcomp ~market-post-cart-badge (&key href count)
|
||||
(a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
|
||||
(i :class "fa fa-shopping-cart" :aria-hidden "true")
|
||||
(span count)))
|
||||
|
||||
(defcomp ~market-shop-label (&key title top-slug sub-div-html)
|
||||
(div :class "font-bold text-xl flex-shrink-0 flex gap-2 items-center"
|
||||
(div (i :class "fa fa-shop") " " title)
|
||||
(div :class "flex flex-col md:flex-row md:gap-2 text-xs"
|
||||
(div top-slug) (raw! sub-div-html))))
|
||||
|
||||
(defcomp ~market-sub-slug (&key sub)
|
||||
(div sub))
|
||||
|
||||
(defcomp ~market-product-label (&key title)
|
||||
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title)))
|
||||
|
||||
(defcomp ~market-admin-link (&key href hx-select)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class "px-2 py-1 text-stone-500 hover:text-stone-700"
|
||||
(i :class "fa fa-cog" :aria-hidden "true")))
|
||||
|
||||
(defcomp ~market-oob-header (&key parent-id child-id row-html)
|
||||
(div :id parent-id :hx-swap-oob "outerHTML" :class "w-full"
|
||||
(div :class "w-full" (raw! row-html)
|
||||
(div :id child-id))))
|
||||
|
||||
(defcomp ~market-header-child (&key inner-html)
|
||||
(div :id "root-header-child" :class "w-full" (raw! inner-html)))
|
||||
19
market/sexp/meta.sexpr
Normal file
19
market/sexp/meta.sexpr
Normal file
@@ -0,0 +1,19 @@
|
||||
;; Market meta/SEO components
|
||||
|
||||
(defcomp ~market-meta-title (&key title)
|
||||
(title title))
|
||||
|
||||
(defcomp ~market-meta-description (&key description)
|
||||
(meta :name "description" :content description))
|
||||
|
||||
(defcomp ~market-meta-canonical (&key href)
|
||||
(link :rel "canonical" :href href))
|
||||
|
||||
(defcomp ~market-meta-og (&key property content)
|
||||
(meta :property property :content content))
|
||||
|
||||
(defcomp ~market-meta-twitter (&key name content)
|
||||
(meta :name name :content content))
|
||||
|
||||
(defcomp ~market-meta-jsonld (&key json)
|
||||
(script :type "application/ld+json" (raw! json)))
|
||||
63
market/sexp/navigation.sexpr
Normal file
63
market/sexp/navigation.sexpr
Normal file
@@ -0,0 +1,63 @@
|
||||
;; Market navigation components
|
||||
|
||||
(defcomp ~market-category-link (&key href hx-select active select-colours label)
|
||||
(div :class "relative nav-group"
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:aria-selected (if active "true" "false")
|
||||
:class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours)
|
||||
label)))
|
||||
|
||||
(defcomp ~market-desktop-category-nav (&key links-html admin-html)
|
||||
(nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center"
|
||||
(raw! links-html) (raw! admin-html)))
|
||||
|
||||
(defcomp ~market-mobile-nav-wrapper (&key items-html)
|
||||
(div :class "px-4 py-2" (div :class "divide-y" (raw! items-html))))
|
||||
|
||||
(defcomp ~market-mobile-all-link (&key href hx-select active select-colours)
|
||||
(a :role "option" :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:aria-selected (if active "true" "false")
|
||||
:class (str "block rounded-lg px-3 py-3 text-base hover:bg-stone-50 " select-colours)
|
||||
(div :class "prose prose-stone max-w-none" "All")))
|
||||
|
||||
(defcomp ~market-mobile-chevron ()
|
||||
(svg :class "w-4 h-4 shrink-0 transition-transform group-open/cat:rotate-180"
|
||||
:viewBox "0 0 20 20" :fill "currentColor"
|
||||
(path :fill-rule "evenodd" :clip-rule "evenodd"
|
||||
:d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z")))
|
||||
|
||||
(defcomp ~market-mobile-cat-summary (&key bg-cls href hx-select select-colours cat-name count-label count-str chevron-html)
|
||||
(summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls)
|
||||
(a :href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:class (str "font-medium " select-colours " flex flex-row gap-2")
|
||||
(div cat-name)
|
||||
(div :aria-label count-label count-str))
|
||||
(raw! chevron-html)))
|
||||
|
||||
(defcomp ~market-mobile-sub-link (&key select-colours active href hx-select label count-label count-str)
|
||||
(a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2")
|
||||
:aria-selected (if active "true" "false")
|
||||
:href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
(div label)
|
||||
(div :aria-label count-label count-str)))
|
||||
|
||||
(defcomp ~market-mobile-subs-panel (&key links-html)
|
||||
(div :class "pb-3 pl-2"
|
||||
(div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100"
|
||||
(div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories"
|
||||
(raw! links-html)))))
|
||||
|
||||
(defcomp ~market-mobile-view-all (&key href hx-select)
|
||||
(div :class "pb-3 pl-2"
|
||||
(a :class "px-2 py-1 rounded hover:bg-stone-100 block"
|
||||
:href href :hx-get href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
|
||||
"View all")))
|
||||
|
||||
(defcomp ~market-mobile-cat-details (&key open summary-html subs-html)
|
||||
(details :class "group/cat py-1" :open open
|
||||
(raw! summary-html) (raw! subs-html)))
|
||||
34
market/sexp/prices.sexpr
Normal file
34
market/sexp/prices.sexpr
Normal file
@@ -0,0 +1,34 @@
|
||||
;; Market price display components
|
||||
|
||||
(defcomp ~market-price-special (&key price)
|
||||
(div :class "text-lg font-semibold text-emerald-700" price))
|
||||
|
||||
(defcomp ~market-price-regular-strike (&key price)
|
||||
(div :class "text-sm line-through text-stone-500" price))
|
||||
|
||||
(defcomp ~market-price-regular (&key price)
|
||||
(div :class "mt-1 text-lg font-semibold" price))
|
||||
|
||||
(defcomp ~market-price-line (&key inner-html)
|
||||
(div :class "mt-1 flex items-baseline gap-2 justify-center" (raw! inner-html)))
|
||||
|
||||
(defcomp ~market-header-price-special-label ()
|
||||
(div :class "text-md font-bold text-emerald-700" "Special price"))
|
||||
|
||||
(defcomp ~market-header-price-special (&key price)
|
||||
(div :class "text-xl font-semibold text-emerald-700" price))
|
||||
|
||||
(defcomp ~market-header-price-strike (&key price)
|
||||
(div :class "text-base text-md line-through text-stone-500" price))
|
||||
|
||||
(defcomp ~market-header-price-regular-label ()
|
||||
(div :class "hidden md:block text-xl font-bold" "Our price"))
|
||||
|
||||
(defcomp ~market-header-price-regular (&key price)
|
||||
(div :class "text-xl font-semibold" price))
|
||||
|
||||
(defcomp ~market-header-rrp (&key rrp)
|
||||
(div :class "text-base text-stone-400" (span "rrp:") " " (span rrp)))
|
||||
|
||||
(defcomp ~market-prices-row (&key inner-html)
|
||||
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" (raw! inner-html)))
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user