diff --git a/shared b/shared index 65c4989..322ae48 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 65c4989d08b85a821a0932e4376f2ab088f50d0c +Subproject commit 322ae481eeb47418245e669c3585f6d2195656da diff --git a/templates/_types/browse/_admin.html b/templates/_types/browse/_admin.html new file mode 100644 index 0000000..e3cf3a2 --- /dev/null +++ b/templates/_types/browse/_admin.html @@ -0,0 +1,7 @@ +{% 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 %} \ No newline at end of file diff --git a/templates/_types/browse/_main_panel.html b/templates/_types/browse/_main_panel.html new file mode 100644 index 0000000..8640ce8 --- /dev/null +++ b/templates/_types/browse/_main_panel.html @@ -0,0 +1,5 @@ + +
+ {% include "_types/browse/_product_cards.html" %} +
+
diff --git a/templates/_types/browse/_product_card.html b/templates/_types/browse/_product_card.html new file mode 100644 index 0000000..b923bc5 --- /dev/null +++ b/templates/_types/browse/_product_card.html @@ -0,0 +1,104 @@ +{% 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 %} +
+ {# ❤️ like button overlay - OUTSIDE the link #} + {% if g.user %} +
+ {% set slug = p.slug %} + {% set liked = p.is_liked or False %} + {% include "_types/browse/like/button.html" %} +
+ {% endif %} + + + + {# Make this relative so we can absolutely position children #} +
+ {% if p.image %} +
+
+ no image + + {% for l in p.labels %} + + {% endfor %} +
+ +
+ {{ p.brand }} +
+
+ + {% else %} +
+
No image
+
    + {% for l in p.labels %} +
  • {{ l }}
  • + {% endfor %} +
+
+ {{ p.brand }} +
+
+ {% endif %} + +
+ {#
{{ prices.rrp(prices_ns) }}
#} + {{ prices.card_price(p)}} + + {% import '_types/product/_cart.html' as _cart %} +
+
+ {{ _cart.add(p.slug, cart)}} +
+ + + +
+ {% for s in p.stickers %} + {{ stick.sticker( + asset_url('stickers/' + s + '.svg'), + s, + True, + size=24, + found=s in selected_stickers + ) }} + {% endfor %} +
+
+ {{ p.title | highlight(search) }} +
+
+
\ No newline at end of file diff --git a/templates/_types/browse/_product_cards.html b/templates/_types/browse/_product_cards.html new file mode 100644 index 0000000..cc8edb3 --- /dev/null +++ b/templates/_types/browse/_product_cards.html @@ -0,0 +1,107 @@ +{% for p in products %} + {% include "_types/browse/_product_card.html" %} +{% endfor %} +{% if page < total_pages|int %} + + + + + +{% else %} +
End of results
+{% endif %} + diff --git a/templates/_types/browse/desktop/_category_selector.html b/templates/_types/browse/desktop/_category_selector.html new file mode 100644 index 0000000..b3c68b6 --- /dev/null +++ b/templates/_types/browse/desktop/_category_selector.html @@ -0,0 +1,40 @@ +{# Categories #} + diff --git a/templates/_types/browse/desktop/_filter/brand.html b/templates/_types/browse/desktop/_filter/brand.html new file mode 100644 index 0000000..616e36e --- /dev/null +++ b/templates/_types/browse/desktop/_filter/brand.html @@ -0,0 +1,40 @@ +{# Brand filter (desktop, single-select) #} + +{# Brands #} + diff --git a/templates/_types/browse/desktop/_filter/labels.html b/templates/_types/browse/desktop/_filter/labels.html new file mode 100644 index 0000000..7a4a41e --- /dev/null +++ b/templates/_types/browse/desktop/_filter/labels.html @@ -0,0 +1,44 @@ + + + +{% import 'macros/stickers.html' as stick %} + + diff --git a/templates/_types/browse/desktop/_filter/like.html b/templates/_types/browse/desktop/_filter/like.html new file mode 100644 index 0000000..c830f98 --- /dev/null +++ b/templates/_types/browse/desktop/_filter/like.html @@ -0,0 +1,38 @@ +{% import 'macros/stickers.html' as stick %} + {% set qs = {"liked": None if liked else True, "page": None}|qs %} + {% set href = (current_local_href ~ qs)|host %} + + {% if liked %} + + {% else %} + + {% endif %} + + {{ liked_count }} + + diff --git a/templates/_types/browse/desktop/_filter/search.html b/templates/_types/browse/desktop/_filter/search.html new file mode 100644 index 0000000..2e0ea8e --- /dev/null +++ b/templates/_types/browse/desktop/_filter/search.html @@ -0,0 +1,44 @@ + +{% macro search(current_local_href,search, search_count, hx_select) -%} + + +
+ + +
+ {% if search %} + {{search_count}} + {% endif %} + {{zap_filter}} +
+
+{% endmacro %} \ No newline at end of file diff --git a/templates/_types/browse/desktop/_filter/sort.html b/templates/_types/browse/desktop/_filter/sort.html new file mode 100644 index 0000000..a4b5404 --- /dev/null +++ b/templates/_types/browse/desktop/_filter/sort.html @@ -0,0 +1,34 @@ + + + + +{% import 'macros/stickers.html' as stick %} +{% set sort_val = sort|default('az', true) %} + + diff --git a/templates/_types/browse/desktop/_filter/stickers.html b/templates/_types/browse/desktop/_filter/stickers.html new file mode 100644 index 0000000..46fd22b --- /dev/null +++ b/templates/_types/browse/desktop/_filter/stickers.html @@ -0,0 +1,46 @@ + + + + +{% import 'macros/stickers.html' as stick %} + + diff --git a/templates/_types/browse/desktop/menu.html b/templates/_types/browse/desktop/menu.html new file mode 100644 index 0000000..893cf2d --- /dev/null +++ b/templates/_types/browse/desktop/menu.html @@ -0,0 +1,37 @@ + {% import '_types/browse/desktop/_filter/search.html' as s %} + {{ s.search(current_local_href, search, search_count, hx_select) }} + +
+
+
{{ category_label }}
+
+ {% include "_types/browse/desktop/_filter/sort.html" %} + + + {% 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 %} + +
+ +
+ + {% include "_types/browse/desktop/_filter/brand.html" %} + +
diff --git a/templates/_types/browse/index.html b/templates/_types/browse/index.html new file mode 100644 index 0000000..015e6b3 --- /dev/null +++ b/templates/_types/browse/index.html @@ -0,0 +1,13 @@ +{% 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 %} diff --git a/templates/_types/browse/like/button.html b/templates/_types/browse/like/button.html new file mode 100644 index 0000000..426bdc1 --- /dev/null +++ b/templates/_types/browse/like/button.html @@ -0,0 +1,20 @@ + diff --git a/templates/_types/browse/mobile/_filter/brand_ul.html b/templates/_types/browse/mobile/_filter/brand_ul.html new file mode 100644 index 0000000..ac15400 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/brand_ul.html @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/templates/_types/browse/mobile/_filter/index.html b/templates/_types/browse/mobile/_filter/index.html new file mode 100644 index 0000000..7c2a615 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/index.html @@ -0,0 +1,30 @@ + + {% 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 %} +
+ + + clear filters + + +
+ {% endif %} +
+ {% include "_types/browse/mobile/_filter/like.html" %} + {% include "_types/browse/mobile/_filter/labels.html" %} +
+ {% include "_types/browse/mobile/_filter/stickers.html" %} + {% include "_types/browse/mobile/_filter/brand_ul.html" %} + diff --git a/templates/_types/browse/mobile/_filter/labels.html b/templates/_types/browse/mobile/_filter/labels.html new file mode 100644 index 0000000..3868d42 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/labels.html @@ -0,0 +1,47 @@ +{% import 'macros/stickers.html' as stick %} + + +{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #} + diff --git a/templates/_types/browse/mobile/_filter/like.html b/templates/_types/browse/mobile/_filter/like.html new file mode 100644 index 0000000..509ea92 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/like.html @@ -0,0 +1,40 @@ +{% import 'macros/stickers.html' as stick %} + \ No newline at end of file diff --git a/templates/_types/browse/mobile/_filter/search.html b/templates/_types/browse/mobile/_filter/search.html new file mode 100644 index 0000000..0f39178 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/search.html @@ -0,0 +1,40 @@ +{% macro search(current_local_href, search, search_count, hx_select) -%} + +
+ + +
+ {% if search %} + {{search_count}} + {% endif %} +
+
+{% endmacro %} \ No newline at end of file diff --git a/templates/_types/browse/mobile/_filter/sort_ul.html b/templates/_types/browse/mobile/_filter/sort_ul.html new file mode 100644 index 0000000..c02de19 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/sort_ul.html @@ -0,0 +1,33 @@ + + + +{% import 'macros/stickers.html' as stick %} + + + \ No newline at end of file diff --git a/templates/_types/browse/mobile/_filter/stickers.html b/templates/_types/browse/mobile/_filter/stickers.html new file mode 100644 index 0000000..fed0927 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/stickers.html @@ -0,0 +1,50 @@ +{% import 'macros/stickers.html' as stick %} + + + +{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #} + diff --git a/templates/_types/browse/mobile/_filter/summary.html b/templates/_types/browse/mobile/_filter/summary.html new file mode 100644 index 0000000..07a86a1 --- /dev/null +++ b/templates/_types/browse/mobile/_filter/summary.html @@ -0,0 +1,120 @@ +{% 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) %} +
+ + +
+ {% if sort %} + + {% endif %} + {% if liked %} +
+ + {% if liked_count is not none %} +
+ {{ liked_count }} +
+ {% endif %} +
+ {% endif %} + {% if selected_labels and selected_labels|length %} + + {% endif %} + {% if selected_stickers and selected_stickers|length %} + + {% endif %} +
+ + {% if selected_brands and selected_brands|length %} + + {% endif %} +
+ {% endcall %} +
+ {% include "_types/browse/mobile/_filter/index.html" %} +
+{% endcall %} diff --git a/templates/_types/cart/_mini.html b/templates/_types/cart/_mini.html new file mode 100644 index 0000000..a8255e4 --- /dev/null +++ b/templates/_types/cart/_mini.html @@ -0,0 +1,45 @@ +{% macro mini(oob=False, count=None) %} +
+ {# cart_count is set by the context processor in all apps. + Cart app computes it from g.cart + calendar_cart_entries; + other apps get it from the cart internal API. + count param allows explicit override when macro is imported without context. #} + {% if count is not none %} + {% set _count = count %} + {% elif cart_count is defined and cart_count is not none %} + {% set _count = cart_count %} + {% elif cart is defined and cart is not none %} + {% set _count = (cart | sum(attribute="quantity")) + ((calendar_cart_entries | length) if calendar_cart_entries else 0) %} + {% else %} + {% set _count = 0 %} + {% endif %} + + {% if _count == 0 %} +
+ + + +
+ {% else %} + + + + + + {{ _count }} + + + {% endif %} +
+{% endmacro %} diff --git a/templates/_types/market/_admin.html b/templates/_types/market/_admin.html new file mode 100644 index 0000000..0b09927 --- /dev/null +++ b/templates/_types/market/_admin.html @@ -0,0 +1,7 @@ +{% 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 %} \ No newline at end of file diff --git a/templates/_types/market/_main_panel.html b/templates/_types/market/_main_panel.html new file mode 100644 index 0000000..87bb965 --- /dev/null +++ b/templates/_types/market/_main_panel.html @@ -0,0 +1,23 @@ +{# Main panel fragment for HTMX navigation - market landing page #} +
+ {% if post.custom_excerpt %} +
+ {{post.custom_excerpt|safe}} +
+ {% endif %} + {% if post.feature_image %} +
+ +
+ {% endif %} +
+ {% if post.html %} + {{post.html|safe}} + {% endif %} +
+
+
diff --git a/templates/_types/market/_title.html b/templates/_types/market/_title.html new file mode 100644 index 0000000..6e8024b --- /dev/null +++ b/templates/_types/market/_title.html @@ -0,0 +1,17 @@ +
+
+ + {{ market_title }} +
+
+
+ {{top_slug or ''}} +
+ {% if sub_slug %} +
+ {{sub_slug}} +
+ {% endif %} +
+
\ No newline at end of file diff --git a/templates/_types/market/admin/_main_panel.html b/templates/_types/market/admin/_main_panel.html new file mode 100644 index 0000000..a354325 --- /dev/null +++ b/templates/_types/market/admin/_main_panel.html @@ -0,0 +1 @@ +market admin \ No newline at end of file diff --git a/templates/_types/market/admin/_nav.html b/templates/_types/market/admin/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/templates/_types/market/admin/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/templates/_types/market/admin/_oob_elements.html b/templates/_types/market/admin/_oob_elements.html new file mode 100644 index 0000000..9b306fd --- /dev/null +++ b/templates/_types/market/admin/_oob_elements.html @@ -0,0 +1,29 @@ +{% 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 %} + + diff --git a/templates/_types/market/admin/header/_header.html b/templates/_types/market/admin/header/_header.html new file mode 100644 index 0000000..950eefc --- /dev/null +++ b/templates/_types/market/admin/header/_header.html @@ -0,0 +1,11 @@ +{% 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 %} \ No newline at end of file diff --git a/templates/_types/market/admin/index.html b/templates/_types/market/admin/index.html new file mode 100644 index 0000000..4798c46 --- /dev/null +++ b/templates/_types/market/admin/index.html @@ -0,0 +1,19 @@ +{% 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 %} diff --git a/templates/_types/market/desktop/_nav.html b/templates/_types/market/desktop/_nav.html new file mode 100644 index 0000000..d4de6e6 --- /dev/null +++ b/templates/_types/market/desktop/_nav.html @@ -0,0 +1,38 @@ + + diff --git a/templates/_types/market/header/_header.html b/templates/_types/market/header/_header.html new file mode 100644 index 0000000..2d92286 --- /dev/null +++ b/templates/_types/market/header/_header.html @@ -0,0 +1,11 @@ +{% 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 %} \ No newline at end of file diff --git a/templates/_types/market/mobile/_nav_panel.html b/templates/_types/market/mobile/_nav_panel.html new file mode 100644 index 0000000..65a9685 --- /dev/null +++ b/templates/_types/market/mobile/_nav_panel.html @@ -0,0 +1,110 @@ +{% from 'macros/glyphs.html' import opener %} +
+
+ {% set all_href = (url_for('market.browse.browse_all') ~ qs)|host %} + {% set all_active = (category_label == 'All Products') %} + +
+ All +
+
+ {% for cat, data in categories.items() %} +
+ + + {% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host %} + + +
{{ cat }}
+
{{ data.count }}
+
+ {{ opener('cat')}} + +
+ +
+ {% if data.subs %} + +
+ +
+ {% 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 %} + +
{{ sub.html_label or sub.name }}
+
{{ sub.count }}
+
+ {% 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%} + +
{{ sub.name }}
+
{{ sub.count }}
+
+ {% endif %} + {% endfor %} +
+
+ {% else %} + {% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%} + View all + {% endif %} +
+
+ {% endfor %} + {% include '_types/market/_admin.html' %} +
+
diff --git a/templates/_types/market/mobile/menu.html b/templates/_types/market/mobile/menu.html new file mode 100644 index 0000000..145b551 --- /dev/null +++ b/templates/_types/market/mobile/menu.html @@ -0,0 +1,6 @@ +{% extends 'mobile/menu.html' %} +{% block menu %} + {% block mobile_menu %} + {% endblock %} + {% include '_types/market/mobile/_nav_panel.html' %} +{% endblock %} diff --git a/templates/_types/post/_nav.html b/templates/_types/post/_nav.html new file mode 100644 index 0000000..037bdcd --- /dev/null +++ b/templates/_types/post/_nav.html @@ -0,0 +1,15 @@ +{% import 'macros/links.html' as links %} + {# Widget-driven container nav — entries, calendars, markets #} + {% if container_nav_widgets %} +
+ {% include '_types/post/admin/_nav_entries.html' %} +
+ {% 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) %} + + {% endcall %} + {% endif %} diff --git a/templates/_types/post/admin/_nav_entries.html b/templates/_types/post/admin/_nav_entries.html new file mode 100644 index 0000000..47290d4 --- /dev/null +++ b/templates/_types/post/admin/_nav_entries.html @@ -0,0 +1,50 @@ + + {# Left scroll arrow - desktop only #} + + + {# Widget-driven nav items container #} +
+
+ {% for wdata in container_nav_widgets %} + {% with ctx=wdata.ctx %} + {% include wdata.widget.template with context %} + {% endwith %} + {% endfor %} +
+
+ + + + {# Right scroll arrow - desktop only #} + diff --git a/templates/_types/post/header/_header.html b/templates/_types/post/header/_header.html new file mode 100644 index 0000000..143e79d --- /dev/null +++ b/templates/_types/post/header/_header.html @@ -0,0 +1,28 @@ +{% import 'macros/links.html' as links %} +{% macro header_row(oob=False) %} + {% call links.menu_row(id='post-row', oob=oob) %} + {% call links.link(url_for('blog.post.post_detail', slug=post.slug), hx_select_search ) %} + {% if post.feature_image %} + + {% endif %} + + {{ post.title | truncate(160, True, '…') }} + + {% endcall %} + {% call links.desktop_nav() %} + {% if page_cart_count is defined and page_cart_count > 0 %} + + + {{ page_cart_count }} + + {% endif %} + {% include '_types/post/_nav.html' %} + {% endcall %} + {% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/templates/_types/product/_main_panel.html b/templates/_types/product/_main_panel.html new file mode 100644 index 0000000..cf8df31 --- /dev/null +++ b/templates/_types/product/_main_panel.html @@ -0,0 +1,131 @@ +{# 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 #} +
+
+ {% if d.images and d.images|length > 0 %} +
+ {# --- like button overlay in top-right --- #} + {% if g.user %} +
+ {% set slug = d.slug %} + {% set liked = liked_by_current_user %} + {% include "_types/browse/like/button.html" %} +
+ {% endif %} + +
+
+ {{ d.title }} + + {% for l in d.labels %} + + {% endfor %} +
+
+ {{ d.brand }} +
+
+ + {% if d.images|length > 1 %} + + + {% endif %} +
+ +
+
+ {% for u in d.images %} + + + {% endfor %} +
+
+ {% else %} +
+ {# Even if no image, still render the like button in the corner for consistency #} + {% if g.user %} +
+ {% set slug = d.slug %} + {% set liked = liked_by_current_user %} + {% include "_types/browse/like/button.html" %} +
+ {% endif %} + + No image +
+ {% endif %} + +
+ {% for s in d.stickers %} + {{ stick.sticker(asset_url('stickers/' + s + '.svg'), s, True, size=40) }} + {% endfor %} +
+
+ +
+ {# Optional extras shown quietly #} +
+ {% if d.price_per_unit or d.price_per_unit_raw %} +
Unit price: {{ prices.price_str(d.price_per_unit, d.price_per_unit_raw, d.price_per_unit_currency) }}
+ {% endif %} + {% if d.case_size_raw %} +
Case size: {{ d.case_size_raw }}
+ {% endif %} + +
+ + {% if d.description_short or d.description_html %} +
+ {% if d.description_short %} +

{{ d.description_short }}

+ {% endif %} + {% if d.description_html %} +
+ {{ d.description_html | safe }} +
+ {% endif %} +
+ {% endif %} + + {% if d.sections and d.sections|length %} +
+ {% for sec in d.sections %} +
+ + {{ sec.title }} + + +
+ {{ sec.html | safe }} +
+
+ {% endfor %} +
+ {% endif %} +
+ +
+
diff --git a/templates/_types/product/_meta.html b/templates/_types/product/_meta.html new file mode 100644 index 0000000..aebb684 --- /dev/null +++ b/templates/_types/product/_meta.html @@ -0,0 +1,106 @@ +{# --- 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 ---- #} +{{ base_product_title }} + +{% if canonical %}{% endif %} + +{# ---- Open Graph ---- #} + + + + +{% if canonical %}{% endif %} +{% if image_url %}{% endif %} + +{# Optional product OG price tags #} +{% if price and price_currency %} + + +{% endif %} +{% if d.brand %} + +{% endif %} +{% if d.sku %} + +{% endif %} + +{# ---- Twitter ---- #} + +{% if site and site().twitter_site %}{% endif %} + + +{% if 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 %} + + diff --git a/templates/_types/product/_oob_elements.html b/templates/_types/product/_oob_elements.html new file mode 100644 index 0000000..589d369 --- /dev/null +++ b/templates/_types/product/_oob_elements.html @@ -0,0 +1,49 @@ +{% 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 %} diff --git a/templates/_types/product/_prices.html b/templates/_types/product/_prices.html new file mode 100644 index 0000000..e56339f --- /dev/null +++ b/templates/_types/product/_prices.html @@ -0,0 +1,33 @@ +{% 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)}} + +
+ {{ _cart.add(d.slug, cart)}} + + {% if prices_ns.sp_val %} +
+ Special price +
+
+ {{ prices.price_str(prices_ns.sp_val, prices_ns.sp_raw, prices_ns.sp_cur) }} +
+ {% if prices_ns.sp_val and prices_ns.rp_val %} +
+ {{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }} +
+ {% endif %} + {% elif prices_ns.rp_val %} + +
+ {{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }} +
+ {% endif %} + {{ prices.rrp(prices_ns) }} + +
+ diff --git a/templates/_types/product/_title.html b/templates/_types/product/_title.html new file mode 100644 index 0000000..0b3be43 --- /dev/null +++ b/templates/_types/product/_title.html @@ -0,0 +1,2 @@ + +
{{ d.title }}
diff --git a/templates/_types/product/admin/_nav.html b/templates/_types/product/admin/_nav.html new file mode 100644 index 0000000..f5c504d --- /dev/null +++ b/templates/_types/product/admin/_nav.html @@ -0,0 +1,2 @@ +{% from 'macros/admin_nav.html' import placeholder_nav %} +{{ placeholder_nav() }} diff --git a/templates/_types/product/admin/_oob_elements.html b/templates/_types/product/admin/_oob_elements.html new file mode 100644 index 0000000..84acac6 --- /dev/null +++ b/templates/_types/product/admin/_oob_elements.html @@ -0,0 +1,40 @@ +{% 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()}} +
+ +
+ {% endcall %} +{% endcall %} + + +{% block mobile_menu %} + {% include '_types/product/admin/_nav.html' %} +{% endblock %} + + +{% block content %} + {% include '_types/product/_main_panel.html' %} +{% endblock %} diff --git a/templates/_types/product/admin/header/_header.html b/templates/_types/product/admin/header/_header.html new file mode 100644 index 0000000..eacdf7d --- /dev/null +++ b/templates/_types/product/admin/header/_header.html @@ -0,0 +1,11 @@ +{% 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 %} \ No newline at end of file diff --git a/templates/_types/product/admin/index.html b/templates/_types/product/admin/index.html new file mode 100644 index 0000000..d1cb714 --- /dev/null +++ b/templates/_types/product/admin/index.html @@ -0,0 +1,39 @@ +{% 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 %} diff --git a/templates/_types/product/header/_header.html b/templates/_types/product/header/_header.html new file mode 100644 index 0000000..6608fce --- /dev/null +++ b/templates/_types/product/header/_header.html @@ -0,0 +1,15 @@ +{% 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 %} + + + diff --git a/templates/_types/product/index.html b/templates/_types/product/index.html new file mode 100644 index 0000000..31ccd88 --- /dev/null +++ b/templates/_types/product/index.html @@ -0,0 +1,61 @@ +{% 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 %} diff --git a/templates/_types/product/prices.html b/templates/_types/product/prices.html new file mode 100644 index 0000000..be9cc4c --- /dev/null +++ b/templates/_types/product/prices.html @@ -0,0 +1,66 @@ +{# ---- 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 %} +
+ rrp: + + {{ ns.rrp }} + +
+ {% 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 '')) %} +
+ {% if sp_val %} +
{{ sp_str }}
+ {% if rp_val %} +
{{ rp_str }}
+ {% endif %} + {% elif rp_val %} +
{{ rp_str }}
+ {% endif %} +
+{% endmacro %} diff --git a/templates/aside_clear.html b/templates/aside_clear.html new file mode 100644 index 0000000..e091ac2 --- /dev/null +++ b/templates/aside_clear.html @@ -0,0 +1,7 @@ + + diff --git a/templates/filter_clear.html b/templates/filter_clear.html new file mode 100644 index 0000000..fc3901e --- /dev/null +++ b/templates/filter_clear.html @@ -0,0 +1,5 @@ +
+
diff --git a/templates/macros/filters.html b/templates/macros/filters.html new file mode 100644 index 0000000..8d13887 --- /dev/null +++ b/templates/macros/filters.html @@ -0,0 +1,117 @@ +{# + 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 %} + + + {{ icon_html | safe }} + {% if count is not none %} + {{ count }} + {% endif %} + +{% 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' %} + + {% 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' %} + + + {% endif %} +{% endmacro %} + +