Files
orion/app/templates/shared/macros/storefront/mini-cart.html
Samir Boulahtit 7245f79f7b refactor: rename shop to storefront for consistency
Rename all "shop" directories and references to "storefront" to match
the API and route naming convention already in use.

Renamed directories:
- app/templates/shop/ → app/templates/storefront/
- static/shop/ → static/storefront/
- app/templates/shared/macros/shop/ → .../macros/storefront/
- docs/frontend/shop/ → docs/frontend/storefront/

Renamed files:
- shop.css → storefront.css
- shop-layout.js → storefront-layout.js

Updated references in:
- app/routes/storefront_pages.py (21 template references)
- app/modules/cms/routes/pages/vendor.py
- app/templates/storefront/base.html (static paths)
- All storefront templates (extends/includes)
- docs/architecture/frontend-structure.md

This aligns the template/static naming with:
- Route file: storefront_pages.py
- API directory: app/api/v1/storefront/
- Module routes: */routes/api/storefront.py
- URL paths: /storefront/*

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:58:28 +01:00

409 lines
15 KiB
HTML

{#
Mini Cart Components
====================
Cart preview dropdown and cart item components.
Usage:
{% from 'shared/macros/shop/mini-cart.html' import mini_cart, cart_icon_button, cart_item %}
#}
{#
Cart Icon Button
================
Cart icon with item count badge for header.
Parameters:
- cart_count_var: Alpine.js expression for cart item count (default: 'cart.items.length')
- toggle_action: Alpine.js action to toggle cart dropdown (default: 'toggleCart()')
- size: 'sm' | 'md' | 'lg' (default: 'md')
Usage:
{{ cart_icon_button() }}
#}
{% macro cart_icon_button(
cart_count_var='cart.items.length',
toggle_action='toggleCart()',
size='md'
) %}
{% set sizes = {
'sm': {'btn': 'p-1.5', 'icon': 'w-5 h-5', 'badge': 'w-4 h-4 text-xs -top-1 -right-1'},
'md': {'btn': 'p-2', 'icon': 'w-6 h-6', 'badge': 'w-5 h-5 text-xs -top-1.5 -right-1.5'},
'lg': {'btn': 'p-2.5', 'icon': 'w-7 h-7', 'badge': 'w-6 h-6 text-sm -top-2 -right-2'}
} %}
{% set s = sizes[size] %}
<button
type="button"
@click="{{ toggle_action }}"
class="relative {{ s.btn }} text-gray-600 dark:text-gray-300 hover:text-purple-600 dark:hover:text-purple-400 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
aria-label="Shopping cart"
>
<span x-html="$icon('shopping-cart', '{{ s.icon }}')"></span>
{# Badge #}
<span
x-show="{{ cart_count_var }} > 0"
x-text="{{ cart_count_var }}"
class="absolute {{ s.badge }} flex items-center justify-center font-bold text-white bg-purple-600 dark:bg-purple-500 rounded-full"
></span>
</button>
{% endmacro %}
{#
Mini Cart Dropdown
==================
Cart preview dropdown showing recent items.
Parameters:
- cart_var: Alpine.js variable for cart object (default: 'cart')
- show_var: Alpine.js variable for dropdown visibility (default: 'showCart')
- max_items: Maximum items to show (default: 3)
- cart_url: URL to full cart page (default: '/cart')
- checkout_url: URL to checkout (default: '/checkout')
Expected cart object structure:
{
items: [
{id, product_id, name, image_url, price, quantity, variant_name}
],
subtotal: number,
item_count: number
}
Usage:
{{ mini_cart() }}
#}
{% macro mini_cart(
cart_var='cart',
show_var='showCart',
max_items=3,
cart_url='/cart',
checkout_url='/checkout'
) %}
<div
x-show="{{ show_var }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-95"
@click.away="{{ show_var }} = false"
class="absolute right-0 top-full mt-2 w-80 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 z-50"
>
{# Header #}
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Shopping Cart</h3>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="{{ cart_var }}.item_count + ' item' + ({{ cart_var }}.item_count !== 1 ? 's' : '')"></p>
</div>
{# Empty State #}
<div x-show="{{ cart_var }}.items.length === 0" class="px-4 py-8 text-center">
<div class="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600">
<span x-html="$icon('shopping-cart', 'w-full h-full')"></span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">Your cart is empty</p>
<a href="/shop" class="inline-block mt-3 text-sm text-purple-600 dark:text-purple-400 hover:underline">
Continue Shopping
</a>
</div>
{# Cart Items #}
<div x-show="{{ cart_var }}.items.length > 0" class="max-h-64 overflow-y-auto">
<template x-for="(item, index) in {{ cart_var }}.items.slice(0, {{ max_items }})" :key="item.id">
{{ cart_item_mini(cart_var=cart_var) }}
</template>
{# More Items Notice #}
<div
x-show="{{ cart_var }}.items.length > {{ max_items }}"
class="px-4 py-2 text-center text-xs text-gray-500 dark:text-gray-400 border-t border-gray-100 dark:border-gray-700"
>
<span x-text="'+ ' + ({{ cart_var }}.items.length - {{ max_items }}) + ' more item' + ({{ cart_var }}.items.length - {{ max_items }} !== 1 ? 's' : '')"></span>
</div>
</div>
{# Footer #}
<div x-show="{{ cart_var }}.items.length > 0" class="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
{# Subtotal #}
<div class="flex justify-between items-center mb-3">
<span class="text-sm text-gray-600 dark:text-gray-400">Subtotal</span>
<span class="text-lg font-bold text-gray-900 dark:text-gray-100" x-text="'€' + {{ cart_var }}.subtotal.toFixed(2)"></span>
</div>
{# Actions #}
<div class="space-y-2">
<a
href="{{ cart_url }}"
class="block w-full px-4 py-2 text-center text-sm font-medium text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900/30 hover:bg-purple-100 dark:hover:bg-purple-900/50 rounded-lg transition-colors"
>
View Cart
</a>
<a
href="{{ checkout_url }}"
class="block w-full px-4 py-2 text-center text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 dark:bg-purple-500 dark:hover:bg-purple-600 rounded-lg transition-colors"
>
Checkout
</a>
</div>
</div>
</div>
{% endmacro %}
{#
Cart Item (Mini version)
========================
Compact cart item for mini cart dropdown.
Parameters:
- cart_var: Cart variable for remove action (default: 'cart')
Usage:
<template x-for="item in cart.items" :key="item.id">
{{ cart_item_mini() }}
</template>
#}
{% macro cart_item_mini(cart_var='cart') %}
<div class="flex gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
{# Image #}
<a :href="item.url" class="flex-shrink-0">
<img
:src="item.image_url"
:alt="item.name"
class="w-14 h-14 object-cover rounded-lg"
loading="lazy"
>
</a>
{# Details #}
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
<a :href="item.url" class="hover:text-purple-600 dark:hover:text-purple-400" x-text="item.name"></a>
</h4>
<p x-show="item.variant_name" class="text-xs text-gray-500 dark:text-gray-400" x-text="item.variant_name"></p>
<div class="flex items-center justify-between mt-1">
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="'Qty: ' + item.quantity"></span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100" x-text="'€' + (item.price * item.quantity).toFixed(2)"></span>
</div>
</div>
{# Remove Button #}
<button
type="button"
@click="removeFromCart(item.id)"
class="flex-shrink-0 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors"
:aria-label="'Remove ' + item.name + ' from cart'"
>
<span x-html="$icon('x', 'w-4 h-4')"></span>
</button>
</div>
{% endmacro %}
{#
Cart Item (Full version)
========================
Full cart item for cart page with quantity controls.
Parameters:
- item_var: Variable name for item (default: 'item')
- index_var: Variable name for index (default: 'index')
- show_image: Show product image (default: true)
- editable: Allow quantity editing (default: true)
Usage:
<template x-for="(item, index) in cart.items" :key="item.id">
{{ cart_item() }}
</template>
#}
{% macro cart_item(
item_var='item',
index_var='index',
show_image=true,
editable=true
) %}
{% from 'shared/macros/inputs.html' import number_stepper %}
<div class="flex gap-4 py-4 border-b border-gray-200 dark:border-gray-700 last:border-0">
{# Image #}
{% if show_image %}
<a :href="{{ item_var }}.url" class="flex-shrink-0">
<img
:src="{{ item_var }}.image_url"
:alt="{{ item_var }}.name"
class="w-20 h-20 md:w-24 md:h-24 object-cover rounded-lg"
loading="lazy"
>
</a>
{% endif %}
{# Details #}
<div class="flex-1 min-w-0">
<div class="flex justify-between gap-4">
<div>
<h3 class="font-medium text-gray-900 dark:text-gray-100">
<a :href="{{ item_var }}.url" class="hover:text-purple-600 dark:hover:text-purple-400" x-text="{{ item_var }}.name"></a>
</h3>
<p x-show="{{ item_var }}.variant_name" class="text-sm text-gray-500 dark:text-gray-400" x-text="{{ item_var }}.variant_name"></p>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="'€' + {{ item_var }}.price.toFixed(2) + ' each'"></p>
</div>
{# Remove Button #}
<button
type="button"
@click="removeFromCart({{ item_var }}.id)"
class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors"
:aria-label="'Remove ' + {{ item_var }}.name"
>
<span x-html="$icon('trash', 'w-5 h-5')"></span>
</button>
</div>
{# Quantity and Total #}
<div class="flex items-center justify-between mt-3">
{% if editable %}
<div>
{{ number_stepper(
model=item_var ~ '.quantity',
min=1,
max=item_var ~ '.max_quantity',
size='sm',
label='Quantity'
) }}
</div>
{% else %}
<span class="text-sm text-gray-600 dark:text-gray-400" x-text="'Qty: ' + {{ item_var }}.quantity"></span>
{% endif %}
<span class="text-lg font-bold text-gray-900 dark:text-gray-100" x-text="'€' + ({{ item_var }}.price * {{ item_var }}.quantity).toFixed(2)"></span>
</div>
{# Low Stock Warning #}
<div
x-show="{{ item_var }}.max_quantity && {{ item_var }}.max_quantity <= 5"
class="flex items-center gap-1 mt-2 text-orange-600 dark:text-orange-400 text-xs"
>
<span x-html="$icon('exclamation', 'w-4 h-4')"></span>
<span x-text="'Only ' + {{ item_var }}.max_quantity + ' left in stock'"></span>
</div>
</div>
</div>
{% endmacro %}
{#
Cart Summary
============
Order summary with totals and checkout button.
Parameters:
- cart_var: Alpine.js variable for cart (default: 'cart')
- show_promo: Show promo code input (default: true)
- show_shipping: Show shipping estimate (default: true)
- checkout_url: Checkout URL (default: '/checkout')
Expected cart structure:
{
subtotal: number,
discount: number,
shipping: number,
tax: number,
total: number,
promo_code: string | null
}
Usage:
{{ cart_summary() }}
#}
{% macro cart_summary(
cart_var='cart',
show_promo=true,
show_shipping=true,
checkout_url='/checkout'
) %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Order Summary</h2>
{# Promo Code #}
{% if show_promo %}
<div class="mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Promo Code</label>
<div class="flex gap-2">
<input
type="text"
x-model="promoCode"
placeholder="Enter code"
class="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<button
type="button"
@click="applyPromoCode()"
:disabled="!promoCode || applyingPromo"
class="px-4 py-2 text-sm font-medium text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900/30 hover:bg-purple-100 dark:hover:bg-purple-900/50 rounded-lg transition-colors disabled:opacity-50"
>
Apply
</button>
</div>
<p x-show="{{ cart_var }}.promo_code" class="mt-2 text-sm text-green-600 dark:text-green-400" x-text="'Code &quot;' + {{ cart_var }}.promo_code + '&quot; applied'"></p>
</div>
{% endif %}
{# Totals #}
<div class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Subtotal</span>
<span class="text-gray-900 dark:text-gray-100" x-text="'€' + {{ cart_var }}.subtotal.toFixed(2)"></span>
</div>
<div x-show="{{ cart_var }}.discount > 0" class="flex justify-between text-green-600 dark:text-green-400">
<span>Discount</span>
<span x-text="'-€' + {{ cart_var }}.discount.toFixed(2)"></span>
</div>
{% if show_shipping %}
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Shipping</span>
<span class="text-gray-900 dark:text-gray-100" x-text="{{ cart_var }}.shipping > 0 ? '€' + {{ cart_var }}.shipping.toFixed(2) : 'Free'"></span>
</div>
{% endif %}
<div x-show="{{ cart_var }}.tax > 0" class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Tax</span>
<span class="text-gray-900 dark:text-gray-100" x-text="'€' + {{ cart_var }}.tax.toFixed(2)"></span>
</div>
<div class="pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="flex justify-between text-lg font-bold">
<span class="text-gray-900 dark:text-gray-100">Total</span>
<span class="text-gray-900 dark:text-gray-100" x-text="'€' + {{ cart_var }}.total.toFixed(2)"></span>
</div>
</div>
</div>
{# Checkout Button #}
<div class="mt-6">
<a
href="{{ checkout_url }}"
class="block w-full px-6 py-3 text-center text-base font-medium text-white bg-purple-600 hover:bg-purple-700 dark:bg-purple-500 dark:hover:bg-purple-600 rounded-lg transition-colors"
>
Proceed to Checkout
</a>
{# Trust Badges #}
<div class="mt-4 flex items-center justify-center gap-4 text-xs text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-1">
<span x-html="$icon('lock-closed', 'w-4 h-4')"></span>
Secure Checkout
</span>
<span class="flex items-center gap-1">
<span x-html="$icon('shield-check', 'w-4 h-4')"></span>
SSL Encrypted
</span>
</div>
</div>
</div>
{% endmacro %}