feat: per-page carts with overview, page-scoped checkout, and split blueprints (Phase 4)
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 21s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 21s
Splits the monolithic cart blueprint into three: cart_overview (GET /), page_cart (/<page_slug>/), and cart_global (webhook, return, add). - New page_cart.py service: get_cart_for_page(), get_calendar_entries_for_page(), get_cart_grouped_by_page() - clear_cart_for_order() and create_order_from_cart() accept page_post_id for scoping - Cart app hydrates page_slug via url_value_preprocessor/url_defaults/hydrate_page - Context processor provides page-scoped cart data when g.page_post exists - Internal API /internal/cart/summary accepts ?page_slug= for page-scoped counts - Overview template shows page cards with item counts and totals - Page cart template reuses show_cart() macro with page-specific header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -103,7 +103,7 @@
|
||||
{% if g.user %}
|
||||
<form
|
||||
method="post"
|
||||
action="{{ url_for('cart.checkout')|host }}"
|
||||
action="{{ url_for('page_cart.page_checkout')|host if page_post is defined and page_post else url_for('cart_global.checkout')|host }}"
|
||||
class="w-full"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<div>
|
||||
<a
|
||||
href="{{ url_for('cart.view_cart')|host }}"
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro header_row(oob=False) %}
|
||||
{% call links.menu_row(id='cart-row', oob=oob) %}
|
||||
{% call links.link(url_for('cart.view_cart'), hx_select_search ) %}
|
||||
{% call links.link(cart_url('/'), hx_select_search ) %}
|
||||
<i class="fa fa-shopping-cart"></i>
|
||||
<h2 class="text-xl font-bold">cart</h2>
|
||||
{% endcall %}
|
||||
|
||||
128
templates/_types/cart/overview/_main_panel.html
Normal file
128
templates/_types/cart/overview/_main_panel.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<div class="max-w-full px-3 py-3 space-y-3">
|
||||
{% if not page_groups or (page_groups | length == 0) %}
|
||||
<div class="rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center">
|
||||
<div class="inline-flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-stone-100 mb-3">
|
||||
<i class="fa fa-shopping-cart text-stone-500 text-sm sm:text-base" aria-hidden="true"></i>
|
||||
</div>
|
||||
<p class="text-base sm:text-lg font-medium text-stone-800">
|
||||
Your cart is empty
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{# Check if there are any items at all across all groups #}
|
||||
{% set ns = namespace(has_items=false) %}
|
||||
{% for grp in page_groups %}
|
||||
{% if grp.cart_items or grp.calendar_entries %}
|
||||
{% set ns.has_items = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not ns.has_items %}
|
||||
<div class="rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center">
|
||||
<div class="inline-flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-stone-100 mb-3">
|
||||
<i class="fa fa-shopping-cart text-stone-500 text-sm sm:text-base" aria-hidden="true"></i>
|
||||
</div>
|
||||
<p class="text-base sm:text-lg font-medium text-stone-800">
|
||||
Your cart is empty
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="space-y-4">
|
||||
{% for grp in page_groups %}
|
||||
{% if grp.cart_items or grp.calendar_entries %}
|
||||
|
||||
{% if grp.post %}
|
||||
{# Page cart card #}
|
||||
<a
|
||||
href="{{ cart_url('/' + grp.post.slug + '/') }}"
|
||||
class="block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
{% if grp.post.feature_image %}
|
||||
<img
|
||||
src="{{ grp.post.feature_image }}"
|
||||
alt="{{ grp.post.title }}"
|
||||
class="h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0"
|
||||
>
|
||||
{% else %}
|
||||
<div class="h-16 w-16 rounded-xl bg-stone-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa fa-store text-stone-400 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base sm:text-lg font-semibold text-stone-900 truncate">
|
||||
{{ grp.post.title }}
|
||||
</h3>
|
||||
|
||||
<div class="mt-1 flex flex-wrap gap-2 text-xs text-stone-600">
|
||||
{% if grp.product_count > 0 %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100">
|
||||
<i class="fa fa-box-open" aria-hidden="true"></i>
|
||||
{{ grp.product_count }} item{{ 's' if grp.product_count != 1 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if grp.calendar_count > 0 %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
{{ grp.calendar_count }} booking{{ 's' if grp.calendar_count != 1 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right flex-shrink-0">
|
||||
<div class="text-lg font-bold text-stone-900">
|
||||
£{{ "%.2f"|format(grp.total) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-emerald-700 font-medium">
|
||||
View cart →
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
{# Orphan bucket (items without a page) #}
|
||||
<div class="rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa fa-shopping-cart text-amber-500 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base sm:text-lg font-semibold text-stone-900">
|
||||
Other items
|
||||
</h3>
|
||||
<div class="mt-1 flex flex-wrap gap-2 text-xs text-stone-600">
|
||||
{% if grp.product_count > 0 %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100">
|
||||
<i class="fa fa-box-open" aria-hidden="true"></i>
|
||||
{{ grp.product_count }} item{{ 's' if grp.product_count != 1 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if grp.calendar_count > 0 %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
{{ grp.calendar_count }} booking{{ 's' if grp.calendar_count != 1 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right flex-shrink-0">
|
||||
<div class="text-lg font-bold text-stone-900">
|
||||
£{{ "%.2f"|format(grp.total) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
24
templates/_types/cart/overview/_oob_elements.html
Normal file
24
templates/_types/cart/overview/_oob_elements.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{# OOB elements for cart overview HTMX navigation #}
|
||||
|
||||
{% 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('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/overview/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
22
templates/_types/cart/overview/index.html
Normal file
22
templates/_types/cart/overview/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/overview/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
4
templates/_types/cart/page/_main_panel.html
Normal file
4
templates/_types/cart/page/_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>
|
||||
27
templates/_types/cart/page/_oob_elements.html
Normal file
27
templates/_types/cart/page/_oob_elements.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'oob_elements.html' %}
|
||||
|
||||
{# OOB elements for page cart HTMX navigation #}
|
||||
|
||||
{% 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('root-header-child', 'cart-header-child', '_types/cart/header/_header.html')}}
|
||||
|
||||
{% from '_types/cart/page/header/_header.html' import page_header_row with context %}
|
||||
{{ page_header_row(oob=True) }}
|
||||
|
||||
{% 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/page/_main_panel.html" %}
|
||||
{% endblock %}
|
||||
25
templates/_types/cart/page/header/_header.html
Normal file
25
templates/_types/cart/page/header/_header.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% macro page_header_row(oob=False) %}
|
||||
{% call links.menu_row(id='page-cart-row', oob=oob) %}
|
||||
{% call links.link(cart_url('/' + page_post.slug + '/'), hx_select_search) %}
|
||||
{% if page_post.feature_image %}
|
||||
<img
|
||||
src="{{ page_post.feature_image }}"
|
||||
class="h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"
|
||||
>
|
||||
{% endif %}
|
||||
<span>
|
||||
{{ page_post.title | truncate(160, True, '...') }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% call links.desktop_nav() %}
|
||||
<a
|
||||
href="{{ cart_url('/') }}"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
|
||||
>
|
||||
<i class="fa fa-arrow-left text-xs" aria-hidden="true"></i>
|
||||
All carts
|
||||
</a>
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
24
templates/_types/cart/page/index.html
Normal file
24
templates/_types/cart/page/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% 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 %}
|
||||
{% from '_types/cart/page/header/_header.html' import page_header_row with context %}
|
||||
{{ page_header_row() }}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block _main_mobile_menu %}
|
||||
{% include '_types/cart/_nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block aside %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_types/cart/page/_main_panel.html' %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user