Delete 391 dead Jinja templates replaced by sx_components/defpage
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m13s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m13s
All app-level templates have been replaced by native sx component builders and defpage declarative routes. Removes ~15,200 lines of dead HTML. Kept: shared/browser templates (errors, ap_social, macros, root layout), account + federation _email/magic_link, federation profile.html chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
{# Card for a single market in the global listing #}
|
||||
{% set pi = page_info.get(market.container_id, {}) %}
|
||||
{% set page_slug = pi.get('slug', '') %}
|
||||
{% set page_title = pi.get('title') %}
|
||||
{% if page_slug %}
|
||||
{% set market_href = market_url('/' ~ page_slug ~ '/' ~ market.slug ~ '/') %}
|
||||
{% else %}
|
||||
{% set market_href = '' %}
|
||||
{% endif %}
|
||||
<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 market_href %}
|
||||
<a href="{{ market_href }}" class="hover:text-emerald-700">
|
||||
<h2 class="text-lg font-semibold text-stone-900">{{ market.name }}</h2>
|
||||
</a>
|
||||
{% else %}
|
||||
<h2 class="text-lg font-semibold text-stone-900">{{ market.name }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if market.description %}
|
||||
<p class="text-sm text-stone-600 mt-1 line-clamp-2">{{ market.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-1.5 mt-3">
|
||||
{% if page_title %}
|
||||
<a href="{{ market_url('/' ~ page_slug ~ '/') }}"
|
||||
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200">
|
||||
{{ page_title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
@@ -1,18 +0,0 @@
|
||||
{% for market in markets %}
|
||||
{% include "_types/all_markets/_card.html" %}
|
||||
{% endfor %}
|
||||
{% if has_more %}
|
||||
{# Infinite scroll sentinel #}
|
||||
{% set next_url = url_for('all_markets.markets_fragment', page=page + 1)|host %}
|
||||
<div
|
||||
id="sentinel-{{ page }}"
|
||||
class="h-4 opacity-0 pointer-events-none"
|
||||
sx-get="{{ next_url }}"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="text-center text-xs text-stone-400">loading...</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,12 +0,0 @@
|
||||
{# Markets grid #}
|
||||
{% if markets %}
|
||||
<div class="max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{% include "_types/all_markets/_cards.html" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-3 py-12 text-center text-stone-400">
|
||||
<i class="fa fa-store text-4xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-lg">No markets available</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="pb-8"></div>
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/all_markets/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,7 +0,0 @@
|
||||
{% import "macros/links.html" as links %}
|
||||
{% if g.rights.admin %}
|
||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||
{{admin_nav_item(
|
||||
url_for('market.browse.product.admin', product_slug=slug)
|
||||
)}}
|
||||
{% endif %}
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3">
|
||||
{% include "_types/browse/_product_cards.html" %}
|
||||
</div>
|
||||
<div class="pb-8"></div>
|
||||
@@ -1,37 +0,0 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{# OOB elements for HTMX navigation - all elements that need updating #}
|
||||
|
||||
{# Import shared OOB macros #}
|
||||
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
{# Header with app title - includes cart-mini, navigation, and market-specific header #}
|
||||
|
||||
{% block oobs %}
|
||||
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('post-header-child', 'market-header-child', '_types/market/header/_header.html')}}
|
||||
|
||||
{% from '_types/post/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/market/mobile/_nav_panel.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{# Filter container with child summary - from browse/index.html child_summary block #}
|
||||
{% block filter %}
|
||||
{% include "_types/browse/mobile/_filter/summary.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% include "_types/browse/desktop/menu.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/browse/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
@@ -1,104 +0,0 @@
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% import '_types/product/prices.html' as prices %}
|
||||
{% set prices_ns = namespace() %}
|
||||
{{ prices.set_prices(p, prices_ns) }}
|
||||
{% set item_href = url_for('market.browse.product.product_detail', product_slug=p.slug)|host %}
|
||||
<div class="flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative">
|
||||
{# ❤️ like button overlay - OUTSIDE the link #}
|
||||
{% if g.user %}
|
||||
<div class="absolute top-2 right-2 z-10 text-6xl md:text-xl">
|
||||
{% set slug = p.slug %}
|
||||
{% set liked = p.is_liked or False %}
|
||||
{% include "_types/browse/like/button.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
href="{{ item_href }}"
|
||||
sx-get="{{ item_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class=""
|
||||
>
|
||||
|
||||
{# Make this relative so we can absolutely position children #}
|
||||
<div class="w-full aspect-square bg-stone-100 relative">
|
||||
{% if p.image %}
|
||||
<figure class="inline-block w-full h-full">
|
||||
<div class="relative w-full h-full">
|
||||
<img
|
||||
src="{{ p.image }}"
|
||||
alt="no image"
|
||||
class="absolute inset-0 w-full h-full object-contain object-top"
|
||||
loading="lazy" decoding="async" fetchpriority="low"
|
||||
/>
|
||||
|
||||
{% for l in p.labels %}
|
||||
<img
|
||||
src="{{ asset_url('labels/' + l + '.svg') }}"
|
||||
alt=""
|
||||
class="pointer-events-none absolute inset-0 w-full h-full object-contain object-top"
|
||||
/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<figcaption class="
|
||||
mt-2 text-sm text-center
|
||||
{{ 'bg-yellow-200' if p.brand in selected_brands else '' }}
|
||||
text-stone-600
|
||||
">
|
||||
{{ p.brand }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
{% else %}
|
||||
<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</div>
|
||||
<ul class="flex flex-row gap-1">
|
||||
{% for l in p.labels %}
|
||||
<li>{{ l }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]">
|
||||
{{ p.brand }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{# <div>{{ prices.rrp(prices_ns) }}</div> #}
|
||||
{{ prices.card_price(p)}}
|
||||
|
||||
{% import '_types/product/_cart.html' as _cart %}
|
||||
</a>
|
||||
<div class="flex justify-center">
|
||||
{{ _cart.add(p.slug, cart)}}
|
||||
</div>
|
||||
|
||||
|
||||
<a
|
||||
href="{{ item_href }}"
|
||||
sx-get="{{ item_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<div class="flex flex-row justify-center gap-2 p-2">
|
||||
{% for s in p.stickers %}
|
||||
{{ stick.sticker(
|
||||
asset_url('stickers/' + s + '.svg'),
|
||||
s,
|
||||
True,
|
||||
size=24,
|
||||
found=s in selected_stickers
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]">
|
||||
{{ p.title | highlight(search) }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
{% for p in products %}
|
||||
{% include "_types/browse/_product_card.html" %}
|
||||
{% endfor %}
|
||||
{% if page < total_pages|int %}
|
||||
|
||||
|
||||
<div
|
||||
id="sentinel-{{ page }}-m"
|
||||
class="block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
|
||||
sx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host }}"
|
||||
sx-trigger="intersect once delay:250ms, sentinelmobile:retry"
|
||||
sx-swap="outerHTML"
|
||||
sx-media="(max-width: 767px)"
|
||||
sx-retry="exponential:1000:30000"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{% include "sentinel/mobile_content.html" %}
|
||||
</div>
|
||||
<!-- DESKTOP sentinel (custom scroll container) -->
|
||||
<div
|
||||
id="sentinel-{{ page }}-d"
|
||||
class="hidden md:block h-4 opacity-0 pointer-events-none"
|
||||
sx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host}}"
|
||||
sx-trigger="intersect once delay:250ms, sentinel:retry"
|
||||
sx-swap="outerHTML"
|
||||
sx-retry="exponential:1000:30000"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{% include "sentinel/desktop_content.html" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-span-full mt-4 text-center text-xs text-stone-400">End of results</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{# Categories #}
|
||||
<nav aria-label="Categories"
|
||||
class="rounded-xl border bg-white shadow-sm min-h-0">
|
||||
<ul class="divide-y">
|
||||
{% set top_active = (sub_slug is not defined or sub_slug is none or sub_slug == '') %}
|
||||
{% set href = (url_for('market.browse.browse_top', top_slug=top_slug) ~ qs)|host %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if top_active else 'false' }}"
|
||||
class="block px-4 py-3 text-[15px] transition {{select_colours}}">
|
||||
<div class="prose prose-stone max-w-none">All products</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% for sub in subs_local %}
|
||||
{% set active = (sub.slug == sub_slug) %}
|
||||
{% set href = (url_for('market.browse.browse_sub', top_slug=top_slug, sub_slug=sub.slug) ~ qs)|host %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if active else 'false' }}"
|
||||
class="block px-4 py-3 text-[15px] border-l-4 transition {{select_colours}}"
|
||||
>
|
||||
<div class="prose prose-stone max-w-none">{{ (sub.html_label or sub.name) | safe }}</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -1,40 +0,0 @@
|
||||
{# Brand filter (desktop, single-select) #}
|
||||
|
||||
{# Brands #}
|
||||
<nav aria-label="Brands"
|
||||
class="rounded-xl border bg-white shadow-sm">
|
||||
<h2 class="text-md mt-2 font-semibold">Brands</h2>
|
||||
<ul class="divide-y">
|
||||
{% for b in brands %}
|
||||
{% set is_selected = (b.name in selected_brands) %}
|
||||
{% if is_selected %}
|
||||
{% set brand_href = (current_local_href ~ {"remove_brand": b.name, "page": None}|qs)|host %}
|
||||
{% else %}
|
||||
{% set brand_href = (current_local_href ~ {"add_brand": b.name, "page": None}|qs)|host %}
|
||||
{% endif %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ brand_href }}"
|
||||
sx-get="{{ brand_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML" sx-push-url="true" hx-on:htmx:afterSwap="this.closest('details')?.removeAttribute('open')"
|
||||
class="flex items-center gap-2 px-2 py-2 rounded transition {% if is_selected %} bg-stone-900 text-white {% else %} hover:bg-stone-50 {% endif %}">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded border {% if is_selected %} border-stone-900 bg-stone-900 text-white {% else %} border-stone-300 {% endif %}">
|
||||
{% if is_selected %}
|
||||
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true">
|
||||
<path d="M5 13l4 4L19 7" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="flex-1 text-sm">{{ b.name }}</span>
|
||||
|
||||
{% if b.count is not none %}
|
||||
<span class="{% if b.count==0 %}text-lg text-red-500{% else %}text-sm{% endif %} {% if is_selected %}opacity-90{% else %}text-stone-500{% endif %}">{{ b.count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
|
||||
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
<ul
|
||||
id="labels-details-desktop"
|
||||
class="flex justify-center p-0 m-0 gap-2"
|
||||
>
|
||||
{% for s in labels %}
|
||||
{% set is_on = (selected_labels and (s.name|lower in selected_labels)) %}
|
||||
{% set qs = {"remove_label": s.name, "page":None}|qs if is_on
|
||||
else {"add_label": s.name, "page":None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
class="flex w-full h-full flex-col items-center justify-center py-2"
|
||||
>
|
||||
<!-- col 1: icon -->
|
||||
{{ stick.sticker(asset_url('nav-labels/' + s.name + '.svg'), s.name, is_on)}}
|
||||
|
||||
|
||||
<!-- col 3: count (right aligned) -->
|
||||
{% if s.count is not none %}
|
||||
<span class="
|
||||
{{'text-xs text-stone-500' if s.count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
leading-none justify-self-end tabular-nums">
|
||||
{{ s.count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -1,38 +0,0 @@
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% set qs = {"liked": None if liked else True, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if liked else 'false' }}"
|
||||
title="liked" aria-label="liked"
|
||||
class="flex flex-col items-center justify-start p-1 rounded hover:bg-stone-50"
|
||||
{% if liked %}
|
||||
aria-label="liked and unliked"
|
||||
{% else %}
|
||||
aria-label="just liked"
|
||||
{% endif %}
|
||||
>
|
||||
{% if liked %}
|
||||
<i aria-hidden="true"
|
||||
class="fa-solid fa-heart text-red-500 text-[40px] leading-none"
|
||||
></i>
|
||||
{% else %}
|
||||
<i aria-hidden="true"
|
||||
class="fa-solid fa-heart text-stone-300 text-[40px] leading-none"
|
||||
></i>
|
||||
{% endif %}
|
||||
<span class="
|
||||
{{'text-[10px] text-stone-500' if liked_count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums
|
||||
"
|
||||
aria_label="liked count"
|
||||
>
|
||||
{{ liked_count }}
|
||||
</span>
|
||||
</a>
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
{% macro search(current_local_href,search, search_count, hx_select) -%}
|
||||
<!-- Search (1/3 width → 4/12 columns) -->
|
||||
<!-- nb this does NOT oob itself!! -->
|
||||
<div
|
||||
id="search-desktop-wrapper"
|
||||
class="flex flex-row gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
id="search-desktop"
|
||||
type="text"
|
||||
name="search"
|
||||
aria-label="search"
|
||||
class="w-full mx-1 my-3 px-3 py-2 text-md rounded-xl border-2 shadow-sm border-white placeholder-shown:border-white [&:not(:placeholder-shown)]:border-yellow-200"
|
||||
sx-preserve
|
||||
value="{{ search|default('', true) }}"
|
||||
placeholder="search"
|
||||
sx-trigger="input changed delay:300ms"
|
||||
sx-target="#main-panel"
|
||||
|
||||
sx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
|
||||
sx-get="{{ (current_local_href ~ {'search': None}|qs)|host}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
sx-headers='{"X-Origin":"search-desktop", "X-Search":"true"}'
|
||||
sx-sync="this:replace"
|
||||
|
||||
autocomplete="off"
|
||||
>
|
||||
|
||||
<div
|
||||
id="search-count-desktop"
|
||||
aria-label="search count"
|
||||
{% if not search_count %}
|
||||
class="text-xl text-red-500"
|
||||
{% endif %}
|
||||
>
|
||||
{% if search %}
|
||||
{{search_count}}
|
||||
{% endif %}
|
||||
{{zap_filter}}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% set sort_val = sort|default('az', true) %}
|
||||
|
||||
<ul
|
||||
id="sort-details-desktop"
|
||||
class="flex w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-0 [&>li]:list-none [&>li]:flex-1"
|
||||
>
|
||||
{% for key,label,icon in sort_options %}
|
||||
{% set is_on = (sort_val == key) %}
|
||||
{% set qs = {"sort": None, "page": None}|qs if is_on
|
||||
else {"sort": key, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
class="flex flex-col items-center justify-center w-full h-full py-2 m-0"
|
||||
>
|
||||
{{ stick.sticker(asset_url(icon), label, is_on) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
<ul
|
||||
id="stickers-details-desktop"
|
||||
class="flex flex-wrap justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1 [&>li]:list-none [&>li]:basis-[20%] [&>li]:max-w-[20%] [&>li]:grow-0"
|
||||
>
|
||||
{% for s in stickers %}
|
||||
{% set is_on = (selected_stickers and (s.name|lower in selected_stickers)) %}
|
||||
{% set qs = {"remove_sticker": s.name, "page": None}|qs if is_on
|
||||
else {"add_sticker": s.name, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host%}
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
class="flex w-full h-full flex-col items-center justify-center py-2"
|
||||
>
|
||||
<span class="text-[11px]">{{s.name|capitalize if s.name|lower != 'sugarfree' else 'Sugar'}}</span>
|
||||
<!-- col 1: icon -->
|
||||
{{ stick.sticker(asset_url('stickers/' + s.name + '.svg'), s.name, is_on)}}
|
||||
|
||||
|
||||
<!-- col 3: count (right aligned) -->
|
||||
{% if s.count is not none %}
|
||||
<span class="
|
||||
{{'text-xs text-stone-500' if s.count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
leading-none justify-self-end tabular-nums">
|
||||
{{ s.count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -1,37 +0,0 @@
|
||||
{% import '_types/browse/desktop/_filter/search.html' as s %}
|
||||
{{ s.search(current_local_href, search, search_count, hx_select) }}
|
||||
|
||||
<div
|
||||
id="category-summary-desktop"
|
||||
hxx-swap-oob="outerHTML"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<div class="text-2xl uppercase tracking-wide text-black-500">{{ category_label }}</div>
|
||||
</div>
|
||||
{% include "_types/browse/desktop/_filter/sort.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">
|
||||
{% include "_types/browse/desktop/_filter/like.html" %}
|
||||
{% if labels %}
|
||||
{% include "_types/browse/desktop/_filter/labels.html" %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% if stickers %}
|
||||
{% include "_types/browse/desktop/_filter/stickers.html" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if subs_local and top_local_href %}
|
||||
{% include "_types/browse/desktop/_category_selector.html" %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="filter-summary-desktop"
|
||||
hxx-swap-oob="outerHTML"
|
||||
>
|
||||
|
||||
{% include "_types/browse/desktop/_filter/brand.html" %}
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
{% extends '_types/market/index.html' %}
|
||||
|
||||
{% block filter %}
|
||||
{% include "_types/browse/mobile/_filter/summary.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% include "_types/browse/desktop/menu.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/browse/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
<button
|
||||
class="flex items-center gap-1 {% if liked %} text-red-600 {% else %} text-stone-300 {% endif %} hover:text-red-600 transition-colors w-[1em] h-[1em]"
|
||||
sx-post="{{ like_url if like_url else url_for('market.browse.product.like_toggle', product_slug=slug)|host }}"
|
||||
sx-target="this"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="false"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-swap-settle="0ms"
|
||||
{% if liked %}
|
||||
aria-label="Unlike this {{ item_type if item_type else 'product' }}"
|
||||
{% else %}
|
||||
aria-label="Like this {{ item_type if item_type else 'product' }}"
|
||||
{% endif %}
|
||||
>
|
||||
{% if liked %}
|
||||
<i aria-hidden="true" class="fa-solid fa-heart"></i>
|
||||
{% else %}
|
||||
<i aria-hidden="true" class="fa-regular fa-heart"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
@@ -1,40 +0,0 @@
|
||||
<nav aria-label="Brands" class="px-4 pb-3" >
|
||||
{% if brands|length %}
|
||||
<h2 class="text-md mt-2 font-semibold">Brands</h2>
|
||||
<ul class="space-y-1 pr-1" >
|
||||
{% for b in brands %}
|
||||
{% set is_selected = (b.name in selected_brands) %}
|
||||
<li>
|
||||
{{current_local_href}}
|
||||
<a
|
||||
{% if is_selected %}
|
||||
{% set href = (current_local_href ~ {"remove_brand": b.name, "page": None}|qs)|host %}
|
||||
{% else %}
|
||||
{% set href = (current_local_href ~ {"add_brand": b.name, "page": None}|qs)|host %}
|
||||
{%endif%}
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
|
||||
class="flex items-center gap-2 my-3 px-2 py-2 rounded transition {% if is_selected %} bg-stone-900 text-white {% else %} hover:bg-stone-50 {% endif %}">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded border {% if is_selected %} border-stone-900 bg-stone-900 text-white {% else %} border-stone-300 {% endif %}">
|
||||
{% if is_selected %}
|
||||
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true">
|
||||
<path d="M5 13l4 4L19 7" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="flex-1 text-sm">{{ b.name }}</span>
|
||||
{% if b.count is not none %}
|
||||
<span class="text-xs {% if is_selected %}opacity-90{% else %}text-stone-500{% endif %}">{{ b.count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
{% include "_types/browse/mobile/_filter/sort_ul.html" %}
|
||||
{% if search or selected_labels|length or selected_stickers|length or selected_brands|length %}
|
||||
{% set href = (current_local_href ~ {"clear_filters": True}|qs)|host %}
|
||||
<div class = "flex flex-row justify-center">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-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
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex flex-row gap-2 justify-center items center">
|
||||
{% include "_types/browse/mobile/_filter/like.html" %}
|
||||
{% include "_types/browse/mobile/_filter/labels.html" %}
|
||||
</div>
|
||||
{% include "_types/browse/mobile/_filter/stickers.html" %}
|
||||
{% include "_types/browse/mobile/_filter/brand_ul.html" %}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
<nav aria-label="labels" class="px-4 pb-3">
|
||||
{# One row only; center when not overflowing; horizontal scroll when needed #}
|
||||
<ul
|
||||
class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar"
|
||||
>
|
||||
|
||||
{% for s in labels %}
|
||||
{% set is_on = (selected_labels and (s.name|lower in selected_labels)) %}
|
||||
|
||||
{% set qs = {"remove_label": s.name, "page": None}|qs if is_on
|
||||
else {"add_label": s.name, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
|
||||
<li class="list-none shrink-0">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
class="flex flex-col items-center justify-start p-1 rounded hover:bg-stone-50">
|
||||
{{ stick.sticker(asset_url('nav-labels/' + s.name + '.svg'), s.name, is_on)}}
|
||||
{% if s.count is not none %}
|
||||
<span class="
|
||||
{{'text-[10px] text-stone-500' if s.count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums
|
||||
"
|
||||
>
|
||||
{{ s.count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #}
|
||||
<style>
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
<nav aria-label="like" class="px-4 pb-3">
|
||||
{% set qs = {"liked": None if liked else True, "page": None}|qs%}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if liked else 'false' }}"
|
||||
title="liked" aria-label="liked"
|
||||
class="flex flex-col items-center justify-start p-1 rounded hover:bg-stone-50"
|
||||
{% if liked %}
|
||||
aria-label="liked and unliked"
|
||||
{% else %}
|
||||
aria-label="just liked"
|
||||
{% endif %}
|
||||
>
|
||||
{% if liked %}
|
||||
<i aria-hidden="true"
|
||||
class="fa-solid fa-heart text-red-500 text-[40px] leading-none"
|
||||
></i>
|
||||
{% else %}
|
||||
<i aria-hidden="true"
|
||||
class="fa-solid fa-heart text-stone-500 text-[40px] leading-none"
|
||||
></i>
|
||||
{% endif %}
|
||||
<span class="
|
||||
{{'text-[10px] text-stone-500' if liked_count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums
|
||||
"
|
||||
aria_label="liked count"
|
||||
>
|
||||
{{ liked_count }}
|
||||
</span>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -1,40 +0,0 @@
|
||||
{% macro search(current_local_href, search, search_count, hx_select) -%}
|
||||
|
||||
<div
|
||||
id="search-mobile-wrapper"
|
||||
class="flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
|
||||
>
|
||||
<input
|
||||
id="search-mobile"
|
||||
type="text"
|
||||
name="search"
|
||||
aria-label="search"
|
||||
class="text-base md:text-sm col-span-5 rounded-md px-3 py-2 mb-2 w-full min-w-0 max-w-full border-2 border-stone-200 placeholder-shown:border-stone-200 [&:not(:placeholder-shown)]:border-yellow-200"
|
||||
sx-preserve
|
||||
value="{{ search|default('', true) }}"
|
||||
placeholder="search"
|
||||
sx-trigger="input changed delay:300ms"
|
||||
sx-target="#main-panel"
|
||||
|
||||
sx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
|
||||
sx-get="{{ (current_local_href ~ {'search': None}|qs)|host }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
sx-headers='{"X-Origin":"search-mobile", "X-Search":"true"}'
|
||||
sx-sync="this:replace"
|
||||
autocomplete="off"
|
||||
>
|
||||
|
||||
<div
|
||||
id="search-count-mobile"
|
||||
aria-label="search count"
|
||||
{% if not search_count %}
|
||||
class="text-xl text-red-500"
|
||||
{% endif %}
|
||||
>
|
||||
{% if search %}
|
||||
{{search_count}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
|
||||
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
|
||||
<nav aria-label="sort" class="px-4 pb-3" >
|
||||
<ul class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar">
|
||||
|
||||
{% for key,label,icon in sort_options %}
|
||||
<li class="list-none">
|
||||
<div class="flex flex-col items-center justify-center w-full">
|
||||
<a
|
||||
{% if sort == key %}
|
||||
{% set href= (current_local_href, {"sort": None, "page": None}|qs )|host %}
|
||||
{% else %}
|
||||
{% set href= (current_local_href ~ {"sort": key, "page": None}|qs )|host %}
|
||||
{% endif %}
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
{{ stick.sticker(asset_url(icon), label, sort==key) }}
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -1,50 +0,0 @@
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
<nav aria-label="stickers" class="px-4 pb-3">
|
||||
{# One row only; center when not overflowing; horizontal scroll when needed #}
|
||||
<ul
|
||||
class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar"
|
||||
>
|
||||
|
||||
{% for s in stickers %}
|
||||
{% set is_on = (selected_stickers and (s.name|lower in selected_stickers)) %}
|
||||
{% set qs = {"remove_sticker": s.name, "page": None}|qs if is_on
|
||||
else {"add_sticker": s.name, "page": None}|qs %}
|
||||
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
|
||||
<li class="list-none shrink-0">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
class="flex flex-col items-center justify-start p-1 rounded hover:bg-stone-50">
|
||||
<span class="text-sm">{{s.name|capitalize if s.name|lower != 'sugarfree' else 'Sugar'}}</span>
|
||||
{{ stick.sticker(asset_url('stickers/' + s.name + '.svg'), s.name, is_on) }}
|
||||
|
||||
{% if s.count is not none %}
|
||||
<span class="
|
||||
{{'text-[10px] text-stone-500' if s.count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums
|
||||
"
|
||||
>
|
||||
{{ s.count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #}
|
||||
<style>
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
@@ -1,120 +0,0 @@
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% import 'macros/layout.html' as layout %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% call layout.details('/filter', 'md:hidden') %}
|
||||
{% call layout.filter_summary("filter-summary-mobile", current_local_href, search, search_count, hx_select) %}
|
||||
<div
|
||||
class="col-span-12 min-w-0 grid grid-cols-1 gap-1 bg-gray-100 px-2"
|
||||
role="list">
|
||||
|
||||
|
||||
<div class="flex flex-row items-start gap-2">
|
||||
{% if sort %}
|
||||
<ul class="relative inline-flex items-center justify-center gap-2">
|
||||
<!-- sticker icon -->
|
||||
{% for k,l,i in sort_options %}
|
||||
{% if k == sort %}
|
||||
{% set key = k %}
|
||||
{% set label = l %}
|
||||
{% set icon = i %}
|
||||
<li role="listitem">
|
||||
{{ stick.sticker(asset_url(icon), label, True)}}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if liked %}
|
||||
<div class="flex flex-col items-center gap-1 pb-1">
|
||||
<i aria-hidden="true"
|
||||
class="fa-solid fa-heart text-red-500 text-[40px] leading-none"
|
||||
></i>
|
||||
{% if liked_count is not none %}
|
||||
<div class="
|
||||
{{'text-[10px] text-stone-500' if liked_count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums"
|
||||
>
|
||||
{{ liked_count }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if selected_labels and selected_labels|length %}
|
||||
<ul class="relative inline-flex items-center justify-center gap-2">
|
||||
{% for st in selected_labels %}
|
||||
{% for s in labels %}
|
||||
{% if st == s.name %}
|
||||
<li role="listitem" class="flex flex-col items-center gap-1 pb-1">
|
||||
{{ stick.sticker(asset_url('nav-labels/' + s.name + '.svg'), s.name, True)}}
|
||||
{% if s.count is not none %}
|
||||
<div class="
|
||||
{{'text-[10px] text-stone-500' if s.count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums
|
||||
"
|
||||
>
|
||||
{{ s.count }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if selected_stickers and selected_stickers|length %}
|
||||
<ul class="relative inline-flex items-center justify-center gap-2">
|
||||
{% for st in selected_stickers %}
|
||||
{% for s in stickers %}
|
||||
{% if st == s.name %}
|
||||
<li role="listitem" class="flex flex-col items-center gap-1 pb-1">
|
||||
<!-- sticker icon -->
|
||||
{{ stick.sticker(asset_url('stickers/' + s.name + '.svg'), s.name, True)}}
|
||||
{% if s.count is not none %}
|
||||
<span class="
|
||||
{{'text-[10px] text-stone-500' if s.count != 0 else 'text-md text-red-500 font-bold'}}
|
||||
mt-1 leading-none tabular-nums
|
||||
"
|
||||
>
|
||||
{{ s.count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if selected_brands and selected_brands|length %}
|
||||
<ul class_="w-full grid grid-cols-12 items-center gap-3 px-4 py-3">
|
||||
{% for b in selected_brands %}
|
||||
<li role="listitem" class="flex flex-row items-center gap-2">
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for brand in brands %}
|
||||
{% if brand.name == b %}
|
||||
{% set ns.count = brand.count %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if ns.count %}
|
||||
<div class="text-md">{{ b }}</div>
|
||||
<div class="text-md">{{ ns.count }}</div>
|
||||
{% else %}
|
||||
<div class="text-md text-red-500">{{ b }}</div>
|
||||
<div class="text-xl text-red-500">0</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
<div id="filter-details-mobile" style="display:contents">
|
||||
{% include "_types/browse/mobile/_filter/index.html" %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
@@ -1,7 +0,0 @@
|
||||
{% import "macros/links.html" as links %}
|
||||
{% if g.rights.admin %}
|
||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||
{{admin_nav_item(
|
||||
url_for('market.admin.admin')
|
||||
)}}
|
||||
{% endif %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{# Main panel fragment for HTMX navigation - market landing page #}
|
||||
<article class="relative w-full">
|
||||
{% if post.custom_excerpt %}
|
||||
<div class="w-full text-center italic text-3xl p-2">
|
||||
{{post.custom_excerpt|safe}}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.feature_image %}
|
||||
<div class="mb-3 flex justify-center">
|
||||
<img
|
||||
src="{{ post.feature_image }}"
|
||||
alt=""
|
||||
class="rounded-lg w-full md:w-3/4 object-cover"
|
||||
>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="blog-content p-2">
|
||||
{% if post.html %}
|
||||
{{post.html|safe}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
<div class="pb-8"></div>
|
||||
@@ -1,30 +0,0 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{# OOB elements for HTMX navigation - all elements that need updating #}
|
||||
|
||||
{# Import shared OOB macros #}
|
||||
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
{# Header with app title - includes cart-mini, navigation, and market-specific header #}
|
||||
|
||||
{% block oobs %}
|
||||
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('post-header-child', 'market-header-child', '_types/market/header/_header.html')}}
|
||||
|
||||
{% from '_types/post/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/market/mobile/_nav_panel.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/market/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div
|
||||
class="font-bold text-xl flex-shrink-0 flex gap-2 items-center">
|
||||
<div>
|
||||
<i class="fa fa-shop"></i>
|
||||
{{ market_title }}
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row md:gap-2 text-xs">
|
||||
<div>
|
||||
{{top_slug or ''}}
|
||||
</div>
|
||||
{% if sub_slug %}
|
||||
<div>
|
||||
{{sub_slug}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
market admin
|
||||
@@ -1,2 +0,0 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
@@ -1,29 +0,0 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{# OOB elements for HTMX navigation - all elements that need updating #}
|
||||
|
||||
{# Import shared OOB macros #}
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
{# Header with app title - includes cart-mini, navigation, and market-specific header #}
|
||||
|
||||
{% block oobs %}
|
||||
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('market-header-child', 'market-admin-header-child', '_types/market/admin/header/_header.html')}}
|
||||
|
||||
{% from '_types/market/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/market/admin/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/market/admin/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='market-admin-row', oob=oob) %}
|
||||
{% call links.link(url_for('market.admin.admin'), hx_select_search) %}
|
||||
{{ links.admin() }}
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/market/admin/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
@@ -1,19 +0,0 @@
|
||||
{% extends '_types/market/index.html' %}
|
||||
|
||||
|
||||
{% block market_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('market-admin-header-child', '_types/market/admin/header/_header.html') %}
|
||||
{% block market_admin_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/market/admin/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/market/admin/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,38 +0,0 @@
|
||||
<!-- Desktop nav -->
|
||||
<nav class="hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center">
|
||||
{% set all_href = (url_for('market.browse.browse_all') ~ qs)|host %}
|
||||
{% set all_active = (category_label == 'All Products') %}
|
||||
<div class="relative nav-group">
|
||||
<a
|
||||
href="{{ all_href }}"
|
||||
sx-get="{{ all_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if all_active else 'false' }}"
|
||||
class="block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black {{select_colours}}">
|
||||
All
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% for cat, data in categories.items() %}
|
||||
{% set cat_href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%}
|
||||
{% set cat_active = (cat == category_label) %}
|
||||
<div class="relative nav-group">
|
||||
<a
|
||||
href="{{ cat_href }}"
|
||||
sx-get="{{ cat_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if cat_active else 'false' }}"
|
||||
class="block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black {{select_colours}}"
|
||||
>
|
||||
{{ cat }}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% include '_types/market/_admin.html' %}
|
||||
</nav>
|
||||
@@ -1,11 +0,0 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='market-row', oob=oob) %}
|
||||
{% call links.link(url_for('market.browse.home'), hx_select_search ) %}
|
||||
{% include '_types/market/_title.html' %}
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/market/desktop/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
|
||||
{% block root_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
|
||||
{% call index_row('market-header-child', '_types/market/header/_header.html') %}
|
||||
{% block market_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/market/mobile/_nav_panel.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block aside %}
|
||||
{# No aside on landing page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/market/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto py-8 px-4">
|
||||
<h1 class="text-2xl font-bold mb-6">Markets</h1>
|
||||
|
||||
{% if markets %}
|
||||
<div class="grid gap-4">
|
||||
{% for m in markets %}
|
||||
<a href="/{{ m.page.slug }}/{{ m.slug }}/"
|
||||
class="block p-6 bg-white border border-stone-200 rounded-lg hover:border-stone-400 transition-colors">
|
||||
<h2 class="text-lg font-semibold">{{ m.name }}</h2>
|
||||
{% if m.description %}
|
||||
<p class="text-stone-600 mt-1">{{ m.description }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-stone-500">No markets available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,110 +0,0 @@
|
||||
{% from 'macros/glyphs.html' import opener %}
|
||||
<div class="px-4 py-2">
|
||||
<div class="divide-y">
|
||||
{% set all_href = (url_for('market.browse.browse_all') ~ qs)|host %}
|
||||
{% set all_active = (category_label == 'All Products') %}
|
||||
<a role="option"
|
||||
href="{{ all_href }}"
|
||||
sx-get="{{ all_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if all_active else 'false' }}"
|
||||
class="block rounded-lg px-3 py-3 text-base hover:bg-stone-50 {{select_colours}}">
|
||||
<div class="prose prose-stone max-w-none">
|
||||
All
|
||||
</div>
|
||||
</a>
|
||||
{% for cat, data in categories.items() %}
|
||||
<details
|
||||
class="group/cat py-1"
|
||||
{% if top_slug == (data.slug | lower) %}open{% endif %}
|
||||
>
|
||||
<summary class="flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50 {% if top_slug==(data.slug | lower) %} bg-stone-900 text-white hover:bg-stone-900 {% endif %}">
|
||||
|
||||
{% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host %}
|
||||
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if top_slug==(data.slug | lower) else 'false' }}"
|
||||
class="font-medium {{ select_colours }} flex flex-row gap-2"
|
||||
>
|
||||
<div>{{ cat }}</div>
|
||||
<div aria-label="{{ data.count }} products">{{ data.count }}</div>
|
||||
</a>
|
||||
{{ opener('cat')}}
|
||||
|
||||
</summary>
|
||||
|
||||
<div class="pb-3 pl-2">
|
||||
{% if data.subs %}
|
||||
<!-- Viewport -->
|
||||
<div
|
||||
data-peek-viewport
|
||||
data-peek-size-px="18"
|
||||
data-peek-edge="bottom"
|
||||
data-peek-mask="true"
|
||||
class="m-2 bg-stone-100">
|
||||
<!-- Inner list (no negative margin by default) -->
|
||||
<div data-peek-inner class="grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" aria-label="Subcategories">
|
||||
{% for sub in data.subs %}
|
||||
{% set href = (url_for('market.browse.browse_sub', top_slug=data.slug, sub_slug=sub.slug) ~qs)|host%}
|
||||
{% if top_slug==(data.slug | lower) and sub_slug == sub.slug %}
|
||||
<a
|
||||
class="snap-start px-2 py-3 rounded {{select_colours}} flex flex-row gap-2"
|
||||
aria-selected="{{ 'true' if top_slug==(data.slug | lower) and sub_slug == sub.slug else 'false' }}"
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<div>{{ sub.html_label or sub.name }}</div>
|
||||
<div aria-label="{{ sub.count }} products">{{ sub.count }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for sub in data.subs %}
|
||||
{% if not (top_slug==(data.slug | lower) and sub_slug == sub.slug) %}
|
||||
{% set href = (url_for('market.browse.browse_sub', top_slug=data.slug, sub_slug=sub.slug) ~ qs)|host%}
|
||||
<a
|
||||
class="snap-start px-2 py-3 rounded {{select_colours}} flex flex-row gap-2"
|
||||
aria-selected="{{ 'true' if top_slug==(data.slug | lower) and sub_slug == sub.slug else 'false' }}"
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<div>{{ sub.name }}</div>
|
||||
<div aria-label="{{ sub.count }} products">{{ sub.count }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%}
|
||||
<a class="px-2 py-1 rounded hover:bg-stone-100 block"
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>View all</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% include '_types/market/_admin.html' %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
{% extends 'mobile/menu.html' %}
|
||||
{% block menu %}
|
||||
{% block mobile_menu %}
|
||||
{% endblock %}
|
||||
{% include '_types/market/mobile/_nav_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,13 +0,0 @@
|
||||
{# Card for a single market in a page-scoped listing #}
|
||||
{% set market_href = market_url('/' ~ post.slug ~ '/' ~ market.slug ~ '/') %}
|
||||
<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>
|
||||
<a href="{{ market_href }}" class="hover:text-emerald-700">
|
||||
<h2 class="text-lg font-semibold text-stone-900">{{ market.name }}</h2>
|
||||
</a>
|
||||
|
||||
{% if market.description %}
|
||||
<p class="text-sm text-stone-600 mt-1 line-clamp-2">{{ market.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
@@ -1,18 +0,0 @@
|
||||
{% for market in markets %}
|
||||
{% include "_types/page_markets/_card.html" %}
|
||||
{% endfor %}
|
||||
{% if has_more %}
|
||||
{# Infinite scroll sentinel #}
|
||||
{% set next_url = url_for('page_markets.markets_fragment', page=page + 1)|host %}
|
||||
<div
|
||||
id="sentinel-{{ page }}"
|
||||
class="h-4 opacity-0 pointer-events-none"
|
||||
sx-get="{{ next_url }}"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="text-center text-xs text-stone-400">loading...</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,12 +0,0 @@
|
||||
{# Markets grid for a single page #}
|
||||
{% if markets %}
|
||||
<div class="max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{% include "_types/page_markets/_cards.html" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-3 py-12 text-center text-stone-400">
|
||||
<i class="fa fa-store text-4xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-lg">No markets for this page</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="pb-8"></div>
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
{% block root_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
|
||||
{% block post_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/page_markets/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{# Container nav from fragments (calendars, markets) #}
|
||||
{% if container_nav_html %}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
|
||||
id="entries-calendars-nav-wrapper">
|
||||
{{ container_nav_html | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Admin link #}
|
||||
{% if post and has_access('blog.post.admin.admin') %}
|
||||
{% call links.link(url_for('blog.post.admin.admin', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
{# Left scroll arrow - desktop only #}
|
||||
<button
|
||||
class="entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
aria-label="Scroll left"
|
||||
onclick="document.getElementById('associated-items-container').scrollLeft -= 200">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
{# Widget-driven nav items container #}
|
||||
<div id="associated-items-container"
|
||||
class="overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none"
|
||||
style="scroll-behavior: smooth;"
|
||||
data-scroll-arrows="entries-nav-arrow"
|
||||
onscroll="(function(el){var arrows=document.getElementsByClassName('entries-nav-arrow');var show=window.innerWidth>=640&&el.scrollWidth>el.clientWidth;for(var i=0;i<arrows.length;i++){if(show){arrows[i].classList.remove('hidden');arrows[i].classList.add('flex')}else{arrows[i].classList.add('hidden');arrows[i].classList.remove('flex')}}})(this)">
|
||||
<div class="flex flex-col sm:flex-row gap-1">
|
||||
{% for wdata in container_nav_widgets %}
|
||||
{% with ctx=wdata.ctx %}
|
||||
{% include wdata.widget.template with context %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
{# Right scroll arrow - desktop only #}
|
||||
<button
|
||||
class="entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
aria-label="Scroll right"
|
||||
onclick="document.getElementById('associated-items-container').scrollLeft += 200">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
@@ -1,28 +0,0 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='post-row', oob=oob) %}
|
||||
{% call links.link(blog_url('/' + post.slug + '/'), hx_select_search ) %}
|
||||
{% if post.feature_image %}
|
||||
<img
|
||||
src="{{ post.feature_image }}"
|
||||
class="h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"
|
||||
>
|
||||
{% endif %}
|
||||
<span>
|
||||
{{ post.title | truncate(160, True, '…') }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% if page_cart_count is defined and page_cart_count > 0 %}
|
||||
<a
|
||||
href="{{ cart_url('/' + post.slug + '/') }}"
|
||||
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"></i>
|
||||
<span>{{ page_cart_count }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% include '_types/post/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
@@ -1,17 +0,0 @@
|
||||
{# HTMX response after add-to-cart: OOB-swap the mini cart + product buttons #}
|
||||
{% import '_types/product/_cart.html' as _cart %}
|
||||
|
||||
{# 1. Update mini cart directly — handler already has the cart data #}
|
||||
{% from 'macros/cart_icon.html' import cart_icon %}
|
||||
{{ cart_icon(count=cart | sum(attribute="quantity")) }}
|
||||
|
||||
{# 2. Update add/remove buttons on the product #}
|
||||
{{ _cart.add(d.slug, cart, oob='true') }}
|
||||
|
||||
{# 3. Update cart item row if visible #}
|
||||
{% from '_types/product/_cart.html' import cart_item with context %}
|
||||
{% if item and item.quantity > 0 %}
|
||||
{{ cart_item(oob='true') }}
|
||||
{% elif item %}
|
||||
{{ cart_item(oob='delete') }}
|
||||
{% endif %}
|
||||
@@ -1,250 +0,0 @@
|
||||
{% macro add(slug, cart, oob='false') %}
|
||||
{% set quantity = cart
|
||||
| selectattr('product.slug', 'equalto', slug)
|
||||
| sum(attribute='quantity') %}
|
||||
|
||||
<div id="cart-{{ slug }}" {% if oob=='true' %} sx-swap-oob="{{oob}}" {% endif %}>
|
||||
|
||||
{% if not quantity %}
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
class="rounded flex items-center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<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"></i>
|
||||
|
||||
<!-- black + overlaid in the center -->
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
<div class="rounded flex items-center gap-2">
|
||||
<!-- minus -->
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
type="hidden"
|
||||
name="count"
|
||||
value="{{ quantity - 1 }}"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- basket with quantity badge -->
|
||||
<a
|
||||
class="relative inline-flex items-center justify-center text-emerald-700"
|
||||
href="{{ cart_url('/') }}"
|
||||
>
|
||||
<span class="relative inline-flex items-center justify-center">
|
||||
<i class="fa-solid fa-shopping-cart text-2xl" aria-hidden="true"></i>
|
||||
|
||||
<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 }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- plus -->
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
type="hidden"
|
||||
name="count"
|
||||
value="{{ quantity + 1 }}"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
{% macro cart_item(oob=False) %}
|
||||
|
||||
{% set p = item.product %}
|
||||
{% set unit_price = p.special_price or p.regular_price %}
|
||||
<article
|
||||
id="cart-item-{{p.slug}}"
|
||||
{% if oob %}
|
||||
sx-swap-oob="{{oob}}"
|
||||
{% endif %}
|
||||
class="flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
|
||||
>
|
||||
<div class="w-full sm:w-32 shrink-0 flex justify-center sm:block">
|
||||
{% if p.image %}
|
||||
<img
|
||||
src="{{ p.image }}"
|
||||
alt="{{ p.title }}"
|
||||
class="w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100"
|
||||
loading="lazy"
|
||||
>
|
||||
{% else %}
|
||||
<div
|
||||
class="w-24 h-24 sm:w-32 sm:h-28 rounded-xl border border-dashed border-stone-300 flex items-center justify-center text-xs text-stone-400"
|
||||
>
|
||||
No image
|
||||
</div>'market', 'product', p.slug
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Details #}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3">
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-sm sm:text-base md:text-lg font-semibold text-stone-900">
|
||||
{% set href=url_for('market.browse.product.product_detail', product_slug=p.slug) %}
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx_get="{{href}}"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="hover:text-emerald-700"
|
||||
>
|
||||
{{ p.title }}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{% if p.brand %}
|
||||
<p class="mt-0.5 text-[0.7rem] sm:text-xs text-stone-500">
|
||||
{{ p.brand }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if item.is_deleted %}
|
||||
<p class="mt-2 inline-flex items-center gap-1 text-[0.65rem] sm:text-xs font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5">
|
||||
<i class="fa-solid fa-triangle-exclamation text-[0.6rem]" aria-hidden="true"></i>
|
||||
This item is no longer available or price has changed
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Unit price #}
|
||||
<div class="text-left sm:text-right">
|
||||
{% if unit_price %}
|
||||
{% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %}
|
||||
<p class="text-sm sm:text-base font-semibold text-stone-900">
|
||||
{{ symbol }}{{ "%.2f"|format(unit_price) }}
|
||||
</p>
|
||||
{% if p.special_price and p.special_price != p.regular_price %}
|
||||
<p class="text-xs text-stone-400 line-through">
|
||||
{{ symbol }}{{ "%.2f"|format(p.regular_price) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-xs text-stone-500">No price</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2 sm:gap-4">
|
||||
<div class="flex items-center gap-2 text-xs sm:text-sm text-stone-700">
|
||||
<span class="text-[0.65rem] sm:text-xs uppercase tracking-wide text-stone-500">Quantity</span>
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
method="post"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
type="hidden"
|
||||
name="count"
|
||||
value="{{ item.quantity - 1 }}"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</form>
|
||||
<span class="inline-flex items-center justify-center px-2 py-1 rounded-full bg-stone-100 text-[0.7rem] sm:text-xs font-medium">
|
||||
{{ item.quantity }}
|
||||
</span>
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
method="post"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
type="hidden"
|
||||
name="count"
|
||||
value="{{ item.quantity + 1 }}"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between sm:justify-end gap-3">
|
||||
{% if unit_price %}
|
||||
{% set line_total = unit_price * item.quantity %}
|
||||
{% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %}
|
||||
<p class="text-sm sm:text-base font-semibold text-stone-900">
|
||||
Line total:
|
||||
{{ symbol }}{{ "%.2f"|format(line_total) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{% endmacro %}
|
||||
@@ -1,131 +0,0 @@
|
||||
{# Main panel fragment for HTMX navigation - product detail content #}
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% import '_types/product/prices.html' as prices %}
|
||||
{% set prices_ns = namespace() %}
|
||||
{{ prices.set_prices(d, prices_ns)}}
|
||||
|
||||
{# Product detail grid from content block #}
|
||||
<div class="mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" data-gallery-root>
|
||||
<div class="md:col-span-2">
|
||||
{% if d.images and d.images|length > 0 %}
|
||||
<div class="relative rounded-xl overflow-hidden bg-stone-100">
|
||||
{# --- like button overlay in top-right --- #}
|
||||
{% if g.user %}
|
||||
<div class="absolute top-3 right-5 z-10 text-6xl md:text-xl">
|
||||
{% set slug = d.slug %}
|
||||
{% set liked = liked_by_current_user %}
|
||||
{% include "_types/browse/like/button.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<figure class="inline-block">
|
||||
<div class="relative w-full aspect-square">
|
||||
<img
|
||||
data-main-img
|
||||
src="{{ d.images[0] }}"
|
||||
alt="{{ d.title }}"
|
||||
class="w-full h-full object-contain object-top"
|
||||
loading="eager" decoding="async"
|
||||
/>
|
||||
|
||||
{% for l in d.labels %}
|
||||
<img
|
||||
src="{{ asset_url('labels/' + l + '.svg') }}"
|
||||
alt=""
|
||||
class="pointer-events-none absolute inset-0 w-full h-full object-contain object-top"
|
||||
/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<figcaption class="mt-2 text-sm text-stone-600 text-center">
|
||||
{{ d.brand }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
{% if d.images|length > 1 %}
|
||||
<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 focus:outline-none focus:ring-2 focus:ring-stone-300 text-3xl md:text-4xl"
|
||||
title="Previous">‹</button>
|
||||
<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 focus:outline-none focus:ring-2 focus:ring-stone-300 text-3xl md:text-4xl"
|
||||
title="Next">›</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<div class="mt-3 flex gap-2 overflow-x-auto no-scrollbar">
|
||||
{% for u in d.images %}
|
||||
<button type="button" data-thumb
|
||||
class="shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2"
|
||||
title="Image {{ loop.index }}">
|
||||
<img src="{{ u }}" class="h-16 w-16 object-contain" alt="thumb {{ loop.index }}" loading="lazy" decoding="async">
|
||||
</button>
|
||||
<span data-image-src="{{ u }}" class="hidden"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400">
|
||||
{# Even if no image, still render the like button in the corner for consistency #}
|
||||
{% if g.user %}
|
||||
<div class="absolute top-2 right-2 z-10">
|
||||
{% set slug = d.slug %}
|
||||
{% set liked = liked_by_current_user %}
|
||||
{% include "_types/browse/like/button.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
No image
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="p-2 flex flex-row justify-center gap-2">
|
||||
{% for s in d.stickers %}
|
||||
{{ stick.sticker(asset_url('stickers/' + s + '.svg'), s, True, size=40) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
{# Optional extras shown quietly #}
|
||||
<div class="mt-2 space-y-1 text-sm text-stone-600">
|
||||
{% if d.price_per_unit or d.price_per_unit_raw %}
|
||||
<div>Unit price: {{ prices.price_str(d.price_per_unit, d.price_per_unit_raw, d.price_per_unit_currency) }}</div>
|
||||
{% endif %}
|
||||
{% if d.case_size_raw %}
|
||||
<div>Case size: {{ d.case_size_raw }}</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if d.description_short or d.description_html %}
|
||||
<div class="mt-4 text-stone-800 space-y-3">
|
||||
{% if d.description_short %}
|
||||
<p class="leading-relaxed text-lg">{{ d.description_short }}</p>
|
||||
{% endif %}
|
||||
{% if d.description_html %}
|
||||
<div class="max-w-none text-sm leading-relaxed">
|
||||
{{ d.description_html | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if d.sections and d.sections|length %}
|
||||
<div class="mt-8 space-y-3">
|
||||
{% for sec in d.sections %}
|
||||
<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">{{ sec.title }}</span>
|
||||
<span class="ml-2 text-xl transition-transform group-open:rotate-180">⌄</span>
|
||||
</summary>
|
||||
<div class="px-4 pb-4 max-w-none text-sm leading-relaxed">
|
||||
{{ sec.html | safe }}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="pb-8"></div>
|
||||
@@ -1,106 +0,0 @@
|
||||
{# --- social/meta_product.html --- #}
|
||||
{# Context expected:
|
||||
site, d (Product), request
|
||||
#}
|
||||
|
||||
{# Visibility → robots: index unless soft-deleted #}
|
||||
{% set robots_here = 'noindex,nofollow' if d.deleted_at else 'index,follow' %}
|
||||
|
||||
{# Compute canonical #}
|
||||
{% set _site_url = site().url.rstrip('/') if site and site().url else '' %}
|
||||
{% set _product_path = request.path if request else ('/products/' ~ (d.slug or '')) %}
|
||||
{% set canonical = _site_url ~ _product_path if _site_url else (request.url if request else None) %}
|
||||
|
||||
{# Include common base (charset, viewport, robots default, RSS, Org/WebSite JSON-LD) #}
|
||||
{% set robots_override = robots_here %}
|
||||
{% include 'social/meta_base.html' %}
|
||||
|
||||
{# ---- Titles / descriptions ---- #}
|
||||
{% set base_product_title = d.title or base_title %}
|
||||
{% set og_title = base_product_title %}
|
||||
{% set tw_title = base_product_title %}
|
||||
|
||||
{# Description: prefer short, then HTML stripped #}
|
||||
{% set desc_source = d.description_short
|
||||
or (d.description_html|striptags if d.description_html else '') %}
|
||||
{% set description = (desc_source|trim|replace('\n',' ')|replace('\r',' ')|striptags)|truncate(160, True, '…') %}
|
||||
|
||||
{# ---- Image priority: product image, then first gallery image, then site default ---- #}
|
||||
{% set image_url = d.image
|
||||
or ((d.images|first).url if d.images and (d.images|first).url else None)
|
||||
or (site().default_image if site and site().default_image else None) %}
|
||||
|
||||
{# ---- Price / offer helpers ---- #}
|
||||
{% set price = d.special_price or d.regular_price or d.rrp %}
|
||||
{% set price_currency = d.special_price_currency or d.regular_price_currency or d.rrp_currency %}
|
||||
|
||||
{# ---- Basic meta ---- #}
|
||||
<title>{{ base_product_title }}</title>
|
||||
<meta name="description" content="{{ description }}">
|
||||
{% if canonical %}<link rel="canonical" href="{{ canonical }}">{% endif %}
|
||||
|
||||
{# ---- Open Graph ---- #}
|
||||
<meta property="og:site_name" content="{{ site().title if site and site().title else '' }}">
|
||||
<meta property="og:type" content="product">
|
||||
<meta property="og:title" content="{{ og_title }}">
|
||||
<meta property="og:description" content="{{ description }}">
|
||||
{% if canonical %}<meta property="og:url" content="{{ canonical }}">{% endif %}
|
||||
{% if image_url %}<meta property="og:image" content="{{ image_url }}">{% endif %}
|
||||
|
||||
{# Optional product OG price tags #}
|
||||
{% if price and price_currency %}
|
||||
<meta property="product:price:amount" content="{{ '%.2f'|format(price) }}">
|
||||
<meta property="product:price:currency" content="{{ price_currency }}">
|
||||
{% endif %}
|
||||
{% if d.brand %}
|
||||
<meta property="product:brand" content="{{ d.brand }}">
|
||||
{% endif %}
|
||||
{% if d.sku %}
|
||||
<meta property="product:retailer_item_id" content="{{ d.sku }}">
|
||||
{% endif %}
|
||||
|
||||
{# ---- Twitter ---- #}
|
||||
<meta name="twitter:card" content="{{ 'summary_large_image' if image_url else 'summary' }}">
|
||||
{% if site and site().twitter_site %}<meta name="twitter:site" content="{{ site().twitter_site }}">{% endif %}
|
||||
<meta name="twitter:title" content="{{ tw_title }}">
|
||||
<meta name="twitter:description" content="{{ description }}">
|
||||
{% if image_url %}<meta name="twitter:image" content="{{ image_url }}">{% endif %}
|
||||
|
||||
{# ---- JSON-LD Product ---- #}
|
||||
{% set jsonld = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": d.title,
|
||||
"image": image_url,
|
||||
"description": description,
|
||||
"sku": d.sku,
|
||||
"brand": d.brand,
|
||||
"url": canonical
|
||||
} %}
|
||||
|
||||
{# Brand as proper object if present #}
|
||||
{% if d.brand %}
|
||||
{% set jsonld = jsonld | combine({
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": d.brand
|
||||
}
|
||||
}) %}
|
||||
{% endif %}
|
||||
|
||||
{# Offers if price available #}
|
||||
{% if price and price_currency %}
|
||||
{% set jsonld = jsonld | combine({
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": price,
|
||||
"priceCurrency": price_currency,
|
||||
"url": canonical,
|
||||
"availability": "https://schema.org/InStock"
|
||||
}
|
||||
}) %}
|
||||
{% endif %}
|
||||
|
||||
<script type="application/ld+json">
|
||||
{{ jsonld | tojson }}
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
{# OOB elements for HTMX navigation - product extends browse so use similar structure #}
|
||||
{% import 'macros/layout.html' as layout %}
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% import '_types/product/prices.html' as prices %}
|
||||
{% set prices_ns = namespace() %}
|
||||
{{ prices.set_prices(d, prices_ns)}}
|
||||
|
||||
{# Import shared OOB macros #}
|
||||
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
|
||||
|
||||
{% block oobs %}
|
||||
{% from '_types/market/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('market-header-child', 'product-header-child', '_types/product/header/_header.html')}}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/market/mobile/_nav_panel.html' %}
|
||||
{% include '_types/browse/_admin.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block filter %}
|
||||
{% call layout.details() %}
|
||||
{% call layout.summary('blog-child-header') %}
|
||||
{% endcall %}
|
||||
{% call layout.menu('blog-child-menu') %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
{% call layout.details() %}
|
||||
{% call layout.summary('product-child-header') %}
|
||||
{% endcall %}
|
||||
{% call layout.menu('item-child-menu') %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/product/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,33 +0,0 @@
|
||||
{% import '_types/product/_cart.html' as _cart %}
|
||||
{# ---- Price block ---- #}
|
||||
{% import '_types/product/prices.html' as prices %}
|
||||
{% set prices_ns = namespace() %}
|
||||
{{ prices.set_prices(d, prices_ns)}}
|
||||
|
||||
<div class="flex flex-row items-center justify-between md:gap-2 md:px-2">
|
||||
{{ _cart.add(d.slug, cart)}}
|
||||
|
||||
{% if prices_ns.sp_val %}
|
||||
<div class="text-md font-bold text-emerald-700">
|
||||
Special price
|
||||
</div>
|
||||
<div class="text-xl font-semibold text-emerald-700">
|
||||
{{ prices.price_str(prices_ns.sp_val, prices_ns.sp_raw, prices_ns.sp_cur) }}
|
||||
</div>
|
||||
{% if prices_ns.sp_val and prices_ns.rp_val %}
|
||||
<div class="text-base text-md line-through text-stone-500">
|
||||
{{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif prices_ns.rp_val %}
|
||||
<div class="hidden md:block text-xl font-bold">
|
||||
Our price
|
||||
</div>
|
||||
<div class="text-xl font-semibold">
|
||||
{{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ prices.rrp(prices_ns) }}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<i class="fa fa-shopping-bag" aria-hidden="true"></i>
|
||||
<div>{{ d.title }}</div>
|
||||
@@ -1,2 +0,0 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
|
||||
{# OOB elements for HTMX navigation - all elements that need updating #}
|
||||
{# Import shared OOB macros #}
|
||||
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
|
||||
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
|
||||
|
||||
|
||||
|
||||
{% block oobs %}
|
||||
|
||||
{% from '_types/root/_n/macros.html' import oob_header with context %}
|
||||
{{oob_header('product-header-child', 'product-admin-header-child', '_types/product/admin/header/_header.html')}}
|
||||
|
||||
{% from '_types/product/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% from '_types/root/_n/macros.html' import header with context %}
|
||||
{% call header(id='product-header-child', oob=True) %}
|
||||
{% call header() %}
|
||||
{% from '_types/product/admin/header/_header.html' import header_row with context %}
|
||||
{{header_row()}}
|
||||
<div id="product-admin-header-header-child">
|
||||
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/product/admin/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/product/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='product-admin-row', oob=oob) %}
|
||||
{% call links.link(url_for('market.browse.product.admin', product_slug=d.slug), hx_select_search ) %}
|
||||
admin!!
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/product/admin/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends '_types/product/index.html' %}
|
||||
|
||||
{% import 'macros/layout.html' as layout %}
|
||||
|
||||
{% block product_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('market-header-child', '_types/product/admin/header/_header.html') %}
|
||||
{% block product_admin_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block ___app_title %}
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% call links.menu_row() %}
|
||||
{% call links.link(url_for('market.browse.product.admin', product_slug=slug), hx_select_search) %}
|
||||
{{ links.admin() }}
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/product/admin/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/product/admin/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/product/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='product-row', oob=oob) %}
|
||||
{% call links.link(url_for('market.browse.product.product_detail', product_slug=d.slug), hx_select_search ) %}
|
||||
{% include '_types/product/_title.html' %}
|
||||
{% endcall %}
|
||||
{% include '_types/product/_prices.html' %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/browse/_admin.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
{% extends '_types/browse/index.html' %}
|
||||
|
||||
{% block meta %}
|
||||
{% include '_types/product/_meta.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
{% import '_types/product/prices.html' as prices %}
|
||||
{% set prices_ns = namespace() %}
|
||||
{{ prices.set_prices(d, prices_ns)}}
|
||||
|
||||
|
||||
|
||||
{% block market_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('market-header-child', '_types/product/header/_header.html') %}
|
||||
{% block product_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/browse/_admin.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block filter %}
|
||||
|
||||
{% call layout.details() %}
|
||||
{% call layout.summary('blog-child-header') %}
|
||||
{% block blog_child_summary %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% call layout.menu('blog-child-menu') %}
|
||||
{% block post_child_menu %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
{% call layout.details() %}
|
||||
{% call layout.summary('product-child-header') %}
|
||||
{% block item_child_summary %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% call layout.menu('item-child-menu') %}
|
||||
{% block item_child_menu %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/product/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,66 +0,0 @@
|
||||
{# ---- Price formatting helpers ---- #}
|
||||
{% set _sym = {'GBP':'£','EUR':'€','USD':'$'} %}
|
||||
{% macro price_str(val, raw, cur) -%}
|
||||
{%- if raw -%}
|
||||
{{ raw }}
|
||||
{%- elif val is number -%}
|
||||
{{ (_sym.get(cur) or '') ~ ('%.2f'|format(val)) }}
|
||||
{%- else -%}
|
||||
{{ val or '' }}
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% macro set_prices(item, ns) -%}
|
||||
|
||||
{% set ns.sp_val = item.special_price or (item.oe_list_price and item.oe_list_price.special) %}
|
||||
{% set ns.sp_raw = item.special_price_raw or (item.oe_list_price and item.oe_list_price.special_raw) %}
|
||||
{% set ns.sp_cur = item.special_price_currency or (item.oe_list_price and item.oe_list_price.special_currency) %}
|
||||
|
||||
{% set ns.rp_val = item.regular_price or item.rrp or (item.oe_list_price and item.oe_list_price.rrp) %}
|
||||
{% set ns.rp_raw = item.regular_price_raw or item.rrp_raw or (item.oe_list_price and item.oe_list_price.rrp_raw) %}
|
||||
{% set ns.rp_cur = item.regular_price_currency or item.rrp_currency or (item.oe_list_price and item.oe_list_price.rrp_currency) %}
|
||||
|
||||
{% set ns.case_size_count = (item.case_size_count or 1) %}
|
||||
{% set ns.rrp = item.rrp_raw[0] ~ "%.2f"|format(item.rrp * (ns.case_size_count)) %}
|
||||
{% set ns.rrp_raw = item.rrp_raw %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% macro rrp(ns) -%}
|
||||
{% if ns.rrp %}
|
||||
<div class="text-base md:text-lgtext-stone-400">
|
||||
<span>rrp:</span>
|
||||
<span>
|
||||
{{ ns.rrp }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% macro card_price(item) %}
|
||||
|
||||
|
||||
{# price block unchanged #}
|
||||
{% set _sym = {'GBP':'£','EUR':'€','USD':'$'} %}
|
||||
{% set sp_val = item.special_price or (item.oe_list_price and item.oe_list_price.special) %}
|
||||
{% set sp_raw = item.special_price_raw or (item.oe_list_price and item.oe_list_price.special_raw) %}
|
||||
{% set sp_cur = item.special_price_currency or (item.oe_list_price and item.oe_list_price.special_currency) %}
|
||||
{% set rp_val = item.regular_price or item.rrp or (item.oe_list_price and item.oe_list_price.rrp) %}
|
||||
{% set rp_raw = item.regular_price_raw or item.rrp_raw or (item.oe_list_price and item.oe_list_price.rrp_raw) %}
|
||||
{% set rp_cur = item.regular_price_currency or item.rrp_currency or (item.oe_list_price and item.oe_list_price.rrp_currency) %}
|
||||
{% set sp_str = sp_raw if sp_raw else ( (_sym.get(sp_cur, '') ~ ('%.2f'|format(sp_val))) if sp_val is number else (sp_val or '')) %}
|
||||
{% set rp_str = rp_raw if rp_raw else ( (_sym.get(rp_cur, '') ~ ('%.2f'|format(rp_val))) if rp_val is number else (rp_val or '')) %}
|
||||
<div class="mt-1 flex items-baseline gap-2 justify-center">
|
||||
{% if sp_val %}
|
||||
<div class="text-lg font-semibold text-emerald-700">{{ sp_str }}</div>
|
||||
{% if rp_val %}
|
||||
<div class="text-sm line-through text-stone-500">{{ rp_str }}</div>
|
||||
{% endif %}
|
||||
{% elif rp_val %}
|
||||
<div class="mt-1 text-lg font-semibold">{{ rp_str }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,7 +0,0 @@
|
||||
<aside
|
||||
id="aside"
|
||||
sx-swap-oob="outerHTML"
|
||||
class="hidden"
|
||||
>
|
||||
</aside>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<div
|
||||
id="filter"
|
||||
sx-swap-oob="outerHTML"
|
||||
>
|
||||
</div>
|
||||
@@ -1,117 +0,0 @@
|
||||
{#
|
||||
Unified filter macros for browse/shop pages
|
||||
Consolidates duplicate mobile/desktop filter components
|
||||
#}
|
||||
|
||||
{% macro filter_item(href, is_on, title, icon_html, count=none, variant='desktop') %}
|
||||
{#
|
||||
Generic filter item (works for labels, stickers, etc.)
|
||||
variant: 'desktop' or 'mobile'
|
||||
#}
|
||||
{% set base_class = "flex flex-col items-center justify-center" %}
|
||||
{% if variant == 'mobile' %}
|
||||
{% set item_class = base_class ~ " p-1 rounded hover:bg-stone-50" %}
|
||||
{% set count_class = "text-[10px] text-stone-500 mt-1 leading-none tabular-nums" if count != 0 else "text-md text-red-500 font-bold mt-1 leading-none tabular-nums" %}
|
||||
{% else %}
|
||||
{% set item_class = base_class ~ " py-2 w-full h-full" %}
|
||||
{% set count_class = "text-xs text-stone-500 leading-none justify-self-end tabular-nums" if count != 0 else "text-md text-red-500 font-bold leading-none justify-self-end tabular-nums" %}
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
href="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ title }}"
|
||||
aria-label="{{ title }}"
|
||||
class="{{ item_class }}"
|
||||
>
|
||||
{{ icon_html | safe }}
|
||||
{% if count is not none %}
|
||||
<span class="{{ count_class }}">{{ count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro labels_list(labels, selected_labels, current_local_href, variant='desktop') %}
|
||||
{#
|
||||
Unified labels filter list
|
||||
variant: 'desktop' or 'mobile'
|
||||
#}
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
{% if variant == 'mobile' %}
|
||||
<nav aria-label="labels" class="px-4 pb-3">
|
||||
<ul class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar">
|
||||
{% else %}
|
||||
<ul id="labels-details-desktop" class="flex justify-center p-0 m-0 gap-2" >
|
||||
{% endif %}
|
||||
|
||||
{% for s in labels %}
|
||||
{% set is_on = (selected_labels and (s.name|lower in selected_labels)) %}
|
||||
{% set qs = {"remove_label": s.name, "page": None}|qs if is_on else {"add_label": s.name, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
|
||||
<li class="{{ 'list-none shrink-0' if variant == 'mobile' else '' }}">
|
||||
{{ filter_item(
|
||||
href, is_on, s.name,
|
||||
stick.sticker(asset_url('nav-labels/' ~ s.name ~ '.svg'), s.name, is_on),
|
||||
s.count, variant
|
||||
) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% if variant == 'mobile' %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro stickers_list(stickers, selected_stickers, current_local_href, variant='desktop') %}
|
||||
{#
|
||||
Unified stickers filter list
|
||||
variant: 'desktop' or 'mobile'
|
||||
#}
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
{% if variant == 'mobile' %}
|
||||
<nav aria-label="stickers" class="px-4 pb-3">
|
||||
<ul class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar">
|
||||
{% else %}
|
||||
<ul id="stickers-details-desktop"
|
||||
class="flex flex-wrap justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1 [&>li]:list-none [&>li]:basis-[20%] [&>li]:max-w-[20%] [&>li]:grow-0"
|
||||
>
|
||||
{% endif %}
|
||||
|
||||
{% for s in stickers %}
|
||||
{% set is_on = (selected_stickers and (s.name|lower in selected_stickers)) %}
|
||||
{% set qs = {"remove_sticker": s.name, "page": None}|qs if is_on else {"add_sticker": s.name, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
{% set display_name = s.name|capitalize if s.name|lower != 'sugarfree' else 'Sugar' %}
|
||||
|
||||
<li class="{{ 'list-none shrink-0' if variant == 'mobile' else '' }}">
|
||||
{% set icon_html %}
|
||||
<span class="{{ 'text-sm' if variant == 'mobile' else 'text-[11px]' }}">{{ display_name }}</span>
|
||||
{{ stick.sticker(asset_url('stickers/' ~ s.name ~ '.svg'), s.name, is_on) }}
|
||||
{% endset %}
|
||||
{{ filter_item(href, is_on, s.name, icon_html, s.count, variant) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% if variant == 'mobile' %}
|
||||
</nav>
|
||||
<style>
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user