Own cart domain templates (Phase 6)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m13s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m13s
Cart, order, orders templates moved from shared to cart/templates/. Cross-domain copies: auth header/index, product cart macro. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2
shared
2
shared
Submodule shared updated: 65c4989d08...322ae481ee
12
templates/_types/auth/header/_header.html
Normal file
12
templates/_types/auth/header/_header.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='auth-row', oob=oob) %}
|
||||
{% call links.link(account_url('/'), hx_select_search ) %}
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<div>account</div>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include "_types/auth/_nav.html" %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
18
templates/_types/auth/index.html
Normal file
18
templates/_types/auth/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends oob.extends %}
|
||||
|
||||
|
||||
{% block root_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row(oob.child_id, oob.header) %}
|
||||
{% block auth_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include oob.nav %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include oob.main %}
|
||||
{% endblock %}
|
||||
4
templates/_types/cart/_main_panel.html
Normal file
4
templates/_types/cart/_main_panel.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="max-w-full px-3 py-3 space-y-3">
|
||||
{% from '_types/cart/_cart.html' import show_cart with context %}
|
||||
{{ show_cart() }}
|
||||
</div>
|
||||
45
templates/_types/cart/_mini.html
Normal file
45
templates/_types/cart/_mini.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% macro mini(oob=False, count=None) %}
|
||||
<div id="cart-mini" {% if oob %}hx-swap-oob="{{oob}}"{% endif %} >
|
||||
{# 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 %}
|
||||
<div class="h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0">
|
||||
<a
|
||||
href="{{ blog_url('/') }}"
|
||||
class="h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
|
||||
>
|
||||
<img
|
||||
src="{{ site().logo }}"
|
||||
class="h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<a
|
||||
href="{{ cart_url('/') }}"
|
||||
class="relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
|
||||
>
|
||||
<i class="fa fa-shopping-cart text-5xl" aria-hidden="true"></i>
|
||||
|
||||
<span
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
|
||||
>
|
||||
{{ _count }}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
2
templates/_types/cart/_nav.html
Normal file
2
templates/_types/cart/_nav.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
28
templates/_types/cart/_oob_elements.html
Normal file
28
templates/_types/cart/_oob_elements.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{# OOB elements for HTMX navigation - all elements that need updating #}
|
||||
|
||||
{% 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('root-header-child', 'cart-header-child', '_types/cart/header/_header.html')}}
|
||||
|
||||
{% from '_types/root/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/cart/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/cart/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
38
templates/_types/cart/checkout_error.html
Normal file
38
templates/_types/cart/checkout_error.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends '_types/root/index.html' %}
|
||||
|
||||
{% block filter %}
|
||||
<header class="mb-6 sm:mb-8">
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight">
|
||||
Checkout error
|
||||
</h1>
|
||||
<p class="text-xs sm:text-sm text-stone-600">
|
||||
We tried to start your payment with SumUp but hit a problem.
|
||||
</p>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-full px-3 py-3 space-y-4">
|
||||
<div class="rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2">
|
||||
<p class="font-medium">Something went wrong.</p>
|
||||
<p>
|
||||
{{ error or "Unexpected error while creating the hosted checkout session." }}
|
||||
</p>
|
||||
{% if order %}
|
||||
<p class="text-xs text-rose-800/80">
|
||||
Order ID: <span class="font-mono">#{{ order.id }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
href="{{ cart_url('/') }}"
|
||||
class="inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
|
||||
>
|
||||
<i class="fa fa-shopping-cart mr-2" aria-hidden="true"></i>
|
||||
Back to cart
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
68
templates/_types/cart/checkout_return.html
Normal file
68
templates/_types/cart/checkout_return.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends '_types/root/index.html' %}
|
||||
|
||||
{% block filter %}
|
||||
<header class="mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight">
|
||||
{% if order.status == 'paid' %}
|
||||
Payment received
|
||||
{% elif order.status == 'failed' %}
|
||||
Payment failed
|
||||
{% elif order.status == 'missing' %}
|
||||
Order not found
|
||||
{% else %}
|
||||
Payment status: {{ order.status|default('pending')|capitalize }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p class="text-xs sm:text-sm text-stone-600">
|
||||
{% if order.status == 'paid' %}
|
||||
Thanks for your order.
|
||||
{% elif order.status == 'failed' %}
|
||||
Something went wrong while processing your payment. You can try again below.
|
||||
{% elif order.status == 'missing' %}
|
||||
We couldn't find that order – it may have expired or never been created.
|
||||
{% else %}
|
||||
We’re still waiting for a final confirmation from SumUp.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{# no aside content for now #}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-full px-1 py-1">
|
||||
{% if order %}
|
||||
<div class="rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2">
|
||||
{% include '_types/order/_summary.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800">
|
||||
We couldn’t find that order. If you reached this page from an old link, please start a new order.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include '_types/order/_items.html' %}
|
||||
{% include '_types/order/_calendar_items.html' %}
|
||||
{% include '_types/order/_ticket_items.html' %}
|
||||
|
||||
{% if order.status == 'failed' and order %}
|
||||
<div class="rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2">
|
||||
<p class="font-medium">Your payment was not completed.</p>
|
||||
<p>
|
||||
You can go back to your cart and try checkout again. If the problem persists,
|
||||
please contact us and mention order <span class="font-mono">#{{ order.id }}</span>.
|
||||
</p>
|
||||
</div>
|
||||
{% elif order.status == 'paid' %}
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50/80 p-4 sm:p-6 text-sm text-emerald-900 space-y-2">
|
||||
<p class="font-medium">All done!</p>
|
||||
<p>We’ll start processing your order shortly.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
12
templates/_types/cart/header/_header.html
Normal file
12
templates/_types/cart/header/_header.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='cart-row', oob=oob) %}
|
||||
{% call links.link(cart_url('/'), hx_select_search ) %}
|
||||
<i class="fa fa-shopping-cart"></i>
|
||||
<h2 class="text-xl font-bold">cart</h2>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/cart/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
22
templates/_types/cart/index.html
Normal file
22
templates/_types/cart/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends '_types/root/_index.html' %}
|
||||
|
||||
{% block root_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('cart-header-child', '_types/cart/header/_header.html') %}
|
||||
{% block cart_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/cart/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block aside %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/cart/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
43
templates/_types/order/_calendar_items.html
Normal file
43
templates/_types/order/_calendar_items.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{# --- NEW: calendar bookings in this order --- #}
|
||||
{% if order and calendar_entries %}
|
||||
<section class="mt-6 space-y-3">
|
||||
<h2 class="text-base sm:text-lg font-semibold">
|
||||
Calendar bookings in this order
|
||||
</h2>
|
||||
|
||||
<ul class="divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80">
|
||||
{% for entry in calendar_entries %}
|
||||
<li class="px-4 py-3 flex items-start justify-between text-sm">
|
||||
<div>
|
||||
<div class="font-medium flex items-center gap-2">
|
||||
{{ entry.name }}
|
||||
{# Small status pill #}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium
|
||||
{% if entry.state == 'confirmed' %}
|
||||
bg-emerald-100 text-emerald-800
|
||||
{% elif entry.state == 'provisional' %}
|
||||
bg-amber-100 text-amber-800
|
||||
{% elif entry.state == 'ordered' %}
|
||||
bg-blue-100 text-blue-800
|
||||
{% else %}
|
||||
bg-stone-100 text-stone-700
|
||||
{% endif %}
|
||||
">
|
||||
{{ entry.state|capitalize }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-stone-500">
|
||||
{{ entry.start_at.strftime('%-d %b %Y, %H:%M') }}
|
||||
{% if entry.end_at %}
|
||||
– {{ entry.end_at.strftime('%-d %b %Y, %H:%M') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 font-medium">
|
||||
£{{ "%.2f"|format(entry.cost or 0) }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
51
templates/_types/order/_items.html
Normal file
51
templates/_types/order/_items.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{# Items list #}
|
||||
{% if order and order.items %}
|
||||
<div class="rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6">
|
||||
<h2 class="text-sm sm:text-base font-semibold mb-3">
|
||||
Items
|
||||
</h2>
|
||||
<ul class="divide-y divide-stone-100 text-xs sm:text-sm">
|
||||
{% for item in order.items %}
|
||||
<li>
|
||||
<a class="w-full py-2 flex gap-3" href="{{ market_product_url(item.product.slug) }}">
|
||||
{# Thumbnail #}
|
||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden">
|
||||
{% if item.product and item.product.image %}
|
||||
<img
|
||||
src="{{ item.product.image }}"
|
||||
alt="{{ item.product_title or item.product.title or 'Product image' }}"
|
||||
class="w-full h-full object-contain object-center"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
>
|
||||
{% else %}
|
||||
<div class="w-full h-full flex items-center justify-center text-[9px] text-stone-400">
|
||||
No image
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Text + pricing #}
|
||||
<div class="flex-1 flex justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ item.product_title or (item.product and item.product.title) or 'Unknown product' }}
|
||||
</p>
|
||||
<p class="text-[11px] text-stone-500">
|
||||
Product ID: {{ item.product_id }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right whitespace-nowrap">
|
||||
<p>Qty: {{ item.quantity }}</p>
|
||||
<p>
|
||||
{{ item.currency or order.currency or 'GBP' }}
|
||||
{{ '%.2f'|format(item.unit_price or 0) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
7
templates/_types/order/_main_panel.html
Normal file
7
templates/_types/order/_main_panel.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="max-w-full px-3 py-3 space-y-4">
|
||||
{# Order summary card #}
|
||||
{% include '_types/order/_summary.html' %}
|
||||
{% include '_types/order/_items.html' %}
|
||||
{% include '_types/order/_calendar_items.html' %}
|
||||
|
||||
</div>
|
||||
2
templates/_types/order/_nav.html
Normal file
2
templates/_types/order/_nav.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
30
templates/_types/order/_oob_elements.html
Normal file
30
templates/_types/order/_oob_elements.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% 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('orders-header-child', 'order-header-child', '_types/order/header/_header.html')}}
|
||||
|
||||
{% from '_types/order/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/order/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/order/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
52
templates/_types/order/_summary.html
Normal file
52
templates/_types/order/_summary.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800">
|
||||
<p>
|
||||
<span class="font-medium">Order ID:</span>
|
||||
<span class="font-mono">#{{ order.id }}</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="font-medium">Created:</span>
|
||||
{% if order.created_at %}
|
||||
{{ order.created_at.strftime('%-d %b %Y, %H:%M') }}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="font-medium">Description:</span>
|
||||
{{ order.description or '–' }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="font-medium">Status:</span>
|
||||
<span class="inline-flex items-center rounded-full px-2 py-1 text-[11px] font-medium
|
||||
{% if order.status == 'paid' %}
|
||||
bg-emerald-50 text-emerald-700 border border-emerald-200
|
||||
{% elif order.status == 'failed' %}
|
||||
bg-rose-50 text-rose-700 border border-rose-200
|
||||
{% else %}
|
||||
bg-stone-50 text-stone-700 border border-stone-200
|
||||
{% endif %}
|
||||
">
|
||||
{{ order.status or 'pending' }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="font-medium">Currency:</span>
|
||||
{{ order.currency or 'GBP' }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="font-medium">Total:</span>
|
||||
{% if order.total_amount %}
|
||||
{{ order.currency or 'GBP' }} {{ '%.2f'|format(order.total_amount) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
49
templates/_types/order/_ticket_items.html
Normal file
49
templates/_types/order/_ticket_items.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{# --- Tickets in this order --- #}
|
||||
{% if order and order_tickets %}
|
||||
<section class="mt-6 space-y-3">
|
||||
<h2 class="text-base sm:text-lg font-semibold">
|
||||
Event tickets in this order
|
||||
</h2>
|
||||
|
||||
<ul class="divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80">
|
||||
{% for tk in order_tickets %}
|
||||
<li class="px-4 py-3 flex items-start justify-between text-sm">
|
||||
<div>
|
||||
<div class="font-medium flex items-center gap-2">
|
||||
{{ tk.entry_name }}
|
||||
{# Small status pill #}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium
|
||||
{% if tk.state == 'confirmed' %}
|
||||
bg-emerald-100 text-emerald-800
|
||||
{% elif tk.state == 'reserved' %}
|
||||
bg-amber-100 text-amber-800
|
||||
{% elif tk.state == 'checked_in' %}
|
||||
bg-blue-100 text-blue-800
|
||||
{% else %}
|
||||
bg-stone-100 text-stone-700
|
||||
{% endif %}
|
||||
">
|
||||
{{ tk.state|replace('_', ' ')|capitalize }}
|
||||
</span>
|
||||
</div>
|
||||
{% if tk.ticket_type_name %}
|
||||
<div class="text-xs text-stone-500">{{ tk.ticket_type_name }}</div>
|
||||
{% endif %}
|
||||
<div class="text-xs text-stone-500">
|
||||
{{ tk.entry_start_at.strftime('%-d %b %Y, %H:%M') }}
|
||||
{% if tk.entry_end_at %}
|
||||
– {{ tk.entry_end_at.strftime('%-d %b %Y, %H:%M') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-xs text-stone-400 font-mono mt-0.5">
|
||||
{{ tk.code }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 font-medium">
|
||||
£{{ "%.2f"|format(tk.price or 0) }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
17
templates/_types/order/header/_header.html
Normal file
17
templates/_types/order/header/_header.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='order-row', oob=oob) %}
|
||||
{% call links.link(url_for('orders.order.order_detail', order_id=order.id), hx_select_search ) %}
|
||||
<i class="fa fa-gbp" aria-hidden="true"></i>
|
||||
<div>
|
||||
Order
|
||||
</div>
|
||||
<div>
|
||||
{{ order.id }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/order/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
68
templates/_types/order/index.html
Normal file
68
templates/_types/order/index.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends '_types/orders/index.html' %}
|
||||
|
||||
|
||||
{% block orders_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('order-header-child', '_types/order/header/_header.html') %}
|
||||
{% block order_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/order/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block filter %}
|
||||
<header class="mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4">
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs sm:text-sm text-stone-600">
|
||||
Placed {% if order.created_at %}{{ order.created_at.strftime('%-d %b %Y, %H:%M') }}{% else %}—{% endif %} · Status: {{ order.status or 'pending' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex w-full sm:w-auto justify-start sm:justify-end gap-2">
|
||||
<a
|
||||
href="{{ url_for('orders.list_orders')|host }}"
|
||||
class="inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
|
||||
>
|
||||
<i class="fa-solid fa-list mr-2" aria-hidden="true"></i>
|
||||
All orders
|
||||
</a>
|
||||
|
||||
{# Re-check status button #}
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('orders.order.order_recheck', order_id=order.id)|host }}"
|
||||
class="inline"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
|
||||
>
|
||||
<i class="fa-solid fa-rotate mr-2" aria-hidden="true"></i>
|
||||
Re-check status
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if order.status != 'paid' %}
|
||||
<a
|
||||
href="{{ url_for('orders.order.order_pay', order_id=order.id)|host }}"
|
||||
class="inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
|
||||
>
|
||||
<i class="fa fa-credit-card mr-2" aria-hidden="true"></i>
|
||||
Open payment page
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/order/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% endblock %}
|
||||
26
templates/_types/orders/_main_panel.html
Normal file
26
templates/_types/orders/_main_panel.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="max-w-full px-3 py-3 space-y-3">
|
||||
{% if not orders %}
|
||||
<div class="rounded-2xl border border-dashed border-stone-300 bg-white/80 p-4 sm:p-6 text-sm text-stone-700">
|
||||
No orders yet.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="overflow-x-auto rounded-2xl border border-stone-200 bg-white/80">
|
||||
<table class="min-w-full text-xs sm:text-sm">
|
||||
<thead class="bg-stone-50 border-b border-stone-200 text-stone-600">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-medium">Order</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Created</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Description</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Total</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Status</th>
|
||||
<th class="px-3 py-2 text-left font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# rows + infinite-scroll sentinel #}
|
||||
{% include "_types/orders/_rows.html" %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
2
templates/_types/orders/_nav.html
Normal file
2
templates/_types/orders/_nav.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% from 'macros/admin_nav.html' import placeholder_nav %}
|
||||
{{ placeholder_nav() }}
|
||||
38
templates/_types/orders/_oob_elements.html
Normal file
38
templates/_types/orders/_oob_elements.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% 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('auth-header-child', 'orders-header-child', '_types/orders/header/_header.html')}}
|
||||
|
||||
{% from '_types/auth/header/_header.html' import header_row with context %}
|
||||
{{ header_row(oob=True) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% import '_types/browse/desktop/_filter/search.html' as s %}
|
||||
{{ s.search(current_local_href, search, search_count, hx_select) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block filter %}
|
||||
{% include '_types/orders/_summary.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block mobile_menu %}
|
||||
{% include '_types/orders/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "_types/orders/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
164
templates/_types/orders/_rows.html
Normal file
164
templates/_types/orders/_rows.html
Normal file
@@ -0,0 +1,164 @@
|
||||
{# suma_browser/templates/_types/order/_orders_rows.html #}
|
||||
|
||||
{# --- existing rows, but split into desktop/tablet vs mobile --- #}
|
||||
{% for order in orders %}
|
||||
{# Desktop / tablet table row #}
|
||||
<tr class="hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60">
|
||||
<td class="px-3 py-2 align-top">
|
||||
<span class="font-mono text-[11px] sm:text-xs">#{{ order.id }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top text-stone-700 text-xs sm:text-sm">
|
||||
{% if order.created_at %}
|
||||
{{ order.created_at.strftime('%-d %b %Y, %H:%M') }}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top text-stone-700 text-xs sm:text-sm">
|
||||
{{ order.description or '' }}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-2 align-top text-stone-700 text-xs sm:text-sm">
|
||||
{{ order.currency or 'GBP' }}
|
||||
{{ '%.2f'|format(order.total_amount or 0) }}
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top">
|
||||
{# status pill, roughly matching existing styling #}
|
||||
<span
|
||||
class="
|
||||
inline-flex items-center rounded-full border px-2 py-0.5
|
||||
text-[11px] sm:text-xs
|
||||
{% if (order.status or '').lower() == 'paid' %}
|
||||
border-emerald-300 bg-emerald-50 text-emerald-700
|
||||
{% elif (order.status or '').lower() in ['failed', 'cancelled'] %}
|
||||
border-rose-300 bg-rose-50 text-rose-700
|
||||
{% else %}
|
||||
border-stone-300 bg-stone-50 text-stone-700
|
||||
{% endif %}
|
||||
"
|
||||
>
|
||||
{{ order.status or 'pending' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-0.5 align-top text-right">
|
||||
<a
|
||||
href="{{ url_for('orders.order.order_detail', order_id=order.id)|host }}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Mobile card row #}
|
||||
<tr class="sm:hidden border-t border-stone-100">
|
||||
<td colspan="5" class="px-3 py-3">
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-[11px] text-stone-700">
|
||||
#{{ order.id }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="
|
||||
inline-flex items-center rounded-full border px-2 py-0.5
|
||||
text-[11px]
|
||||
{% if (order.status or '').lower() == 'paid' %}
|
||||
border-emerald-300 bg-emerald-50 text-emerald-700
|
||||
{% elif (order.status or '').lower() in ['failed', 'cancelled'] %}
|
||||
border-rose-300 bg-rose-50 text-rose-700
|
||||
{% else %}
|
||||
border-stone-300 bg-stone-50 text-stone-700
|
||||
{% endif %}
|
||||
"
|
||||
>
|
||||
{{ order.status or 'pending' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text-[11px] text-stone-500 break-words">
|
||||
{{ order.created_at or '' }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="font-medium text-stone-800">
|
||||
{{ order.currency or 'GBP' }}
|
||||
{{ '%.2f'|format(order.total_amount or 0) }}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="{{ url_for('orders.order.order_detail', order_id=order.id)|host }}"
|
||||
class="inline-flex items-center px-2 py-1 text-[11px] rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition shrink-0"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{# --- sentinel / end-of-results --- #}
|
||||
{% if page < total_pages|int %}
|
||||
<tr
|
||||
id="orders-sentinel-{{ page }}"
|
||||
hx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host }}"
|
||||
hx-trigger="intersect once delay:250ms, sentinel:retry"
|
||||
hx-swap="outerHTML"
|
||||
_="
|
||||
init
|
||||
if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end
|
||||
|
||||
on sentinel:retry
|
||||
remove .hidden from .js-loading in me
|
||||
add .hidden to .js-neterr in me
|
||||
set me.style.pointerEvents to 'none'
|
||||
set me.style.opacity to '0'
|
||||
trigger htmx:consume on me
|
||||
call htmx.trigger(me, 'intersect')
|
||||
end
|
||||
|
||||
def backoff()
|
||||
add .hidden to .js-loading in me
|
||||
remove .hidden from .js-neterr in me
|
||||
set myMs to Number(me.dataset.retryMs)
|
||||
if myMs < 10000 then set me.dataset.retryMs to myMs * 2 end
|
||||
js setTimeout(() => htmx.trigger(me, 'sentinel:retry'), myMs)
|
||||
end
|
||||
|
||||
on htmx:beforeRequest
|
||||
set me.style.pointerEvents to 'none'
|
||||
set me.style.opacity to '0'
|
||||
end
|
||||
|
||||
on htmx:afterSwap
|
||||
set me.dataset.retryMs to 1000
|
||||
end
|
||||
|
||||
on htmx:sendError call backoff()
|
||||
on htmx:responseError call backoff()
|
||||
on htmx:timeout call backoff()
|
||||
"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<td colspan="5" class="px-3 py-4">
|
||||
{# Mobile sentinel content #}
|
||||
<div class="block md:hidden h-[60vh] js-mobile-sentinel">
|
||||
{% include "sentinel/mobile_content.html" %}
|
||||
</div>
|
||||
|
||||
{# Desktop sentinel content #}
|
||||
<div class="hidden md:block h-[30vh] js-desktop-sentinel">
|
||||
{% include "sentinel/desktop_content.html" %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-3 py-4 text-center text-xs text-stone-400">
|
||||
End of results
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
11
templates/_types/orders/_summary.html
Normal file
11
templates/_types/orders/_summary.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<header class="mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4">
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs sm:text-sm text-stone-600">
|
||||
Recent orders placed via the checkout.
|
||||
</p>
|
||||
</div>
|
||||
<div class="md:hidden">
|
||||
{% import '_types/browse/mobile/_filter/search.html' as s %}
|
||||
{{ s.search(current_local_href, search, search_count, hx_select) }}
|
||||
</div>
|
||||
</header>
|
||||
14
templates/_types/orders/header/_header.html
Normal file
14
templates/_types/orders/header/_header.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='orders-row', oob=oob) %}
|
||||
{% call links.link(url_for('orders.list_orders'), hx_select_search, ) %}
|
||||
<i class="fa fa-gbp" aria-hidden="true"></i>
|
||||
<div>
|
||||
Orders
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
{% include '_types/orders/_nav.html' %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
29
templates/_types/orders/index.html
Normal file
29
templates/_types/orders/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends '_types/auth/index.html' %}
|
||||
|
||||
|
||||
{% block auth_header_child %}
|
||||
{% from '_types/root/_n/macros.html' import index_row with context %}
|
||||
{% call index_row('orders-header-child', '_types/orders/header/_header.html') %}
|
||||
{% block orders_header_child %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/orders/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block aside %}
|
||||
{% import '_types/browse/desktop/_filter/search.html' as s %}
|
||||
{{ s.search(current_local_href, search, search_count, hx_select) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block filter %}
|
||||
{% include '_types/orders/_summary.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/orders/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
250
templates/_types/product/_cart.html
Normal file
250
templates/_types/product/_cart.html
Normal file
@@ -0,0 +1,250 @@
|
||||
{% macro add(slug, cart, oob='false') %}
|
||||
{% set quantity = cart
|
||||
| selectattr('product.slug', 'equalto', slug)
|
||||
| sum(attribute='quantity') %}
|
||||
|
||||
<div id="cart-{{ slug }}" {% if oob=='true' %} hx-swap-oob="{{oob}}" {% endif %}>
|
||||
|
||||
{% if not quantity %}
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-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"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-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"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-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 %}
|
||||
hx-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}}"
|
||||
hx-target="#main-panel"
|
||||
hx-select ="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-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"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-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"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-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 %}
|
||||
Reference in New Issue
Block a user