feat: storefront subscription access guard + module-driven nav + URL rename

Add StorefrontAccessMiddleware that blocks storefront access for stores
without an active subscription, returning a multilingual unavailable page
(en/fr/de/lb) for page requests and JSON 403 for API requests. Multi-platform
aware: resolves subscription for detected platform with fallback to primary.

Also includes yesterday's session work:
- Module-driven storefront navigation via FrontendType.STOREFRONT menu declarations
- shop/ → storefront/ URL rename across 30+ templates
- Subscription context (tier_code) passed to storefront templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 13:27:31 +01:00
parent 682213fdee
commit 2c710ad416
46 changed files with 1484 additions and 231 deletions

View File

@@ -6,7 +6,13 @@ This module provides shopping cart functionality for customer storefronts.
It is session-based and does not require customer authentication.
"""
from app.modules.base import ModuleDefinition, PermissionDefinition
from app.modules.base import (
MenuItemDefinition,
MenuSectionDefinition,
ModuleDefinition,
PermissionDefinition,
)
from app.modules.enums import FrontendType
# =============================================================================
# Router Lazy Imports
@@ -53,7 +59,24 @@ cart_module = ModuleDefinition(
),
],
# Cart is storefront-only - no admin/store menus needed
menu_items={},
menus={
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="actions",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="cart",
label_key="storefront.actions.cart",
icon="shopping-cart",
route="storefront/cart",
order=20,
),
],
),
],
},
)

View File

@@ -13,7 +13,7 @@
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
<a href="{{ base_url }}storefront/products" class="hover:text-primary">Products</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium">Cart</span>
</div>
@@ -39,7 +39,7 @@
<p class="text-gray-600 dark:text-gray-400 mb-6">
Add some products to get started!
</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
<a href="{{ base_url }}storefront/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
Browse Products
</a>
</div>
@@ -55,8 +55,8 @@
{# Item Image #}
<div class="flex-shrink-0">
<img
:src="item.image_url || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
:src="item.image_url || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="item.name"
class="w-24 h-24 object-cover rounded-lg"
>
@@ -153,7 +153,7 @@
Proceed to Checkout
</button>
<a href="{{ base_url }}shop/products" class="block w-full px-6 py-3 text-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<a href="{{ base_url }}storefront/products" class="block w-full px-6 py-3 text-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
Continue Shopping
</a>
@@ -218,7 +218,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log(`[SHOP] Loading cart for session ${this.sessionId}...`);
const response = await fetch(`/api/v1/shop/cart/${this.sessionId}`);
const response = await fetch(`/api/v1/storefront/cart/${this.sessionId}`);
if (response.ok) {
const data = await response.json();
@@ -245,7 +245,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log('[SHOP] Updating quantity:', productId, newQuantity);
const response = await fetch(
`/api/v1/shop/cart/${this.sessionId}/items/${productId}`,
`/api/v1/storefront/cart/${this.sessionId}/items/${productId}`,
{
method: 'PUT',
headers: {
@@ -280,7 +280,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log('[SHOP] Removing item:', productId);
const response = await fetch(
`/api/v1/shop/cart/${this.sessionId}/items/${productId}`,
`/api/v1/storefront/cart/${this.sessionId}/items/${productId}`,
{
method: 'DELETE'
}
@@ -307,9 +307,9 @@ document.addEventListener('alpine:init', () => {
if (!token) {
// Redirect to login with return URL
window.location.href = '{{ base_url }}shop/account/login?return={{ base_url }}shop/checkout';
window.location.href = '{{ base_url }}storefront/account/login?return={{ base_url }}storefront/checkout';
} else {
window.location.href = '{{ base_url }}shop/checkout';
window.location.href = '{{ base_url }}storefront/checkout';
}
}
};

View File

@@ -125,6 +125,36 @@ catalog_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="nav",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="products",
label_key="storefront.nav.products",
icon="shopping-bag",
route="storefront/products",
order=10,
),
],
),
MenuSectionDefinition(
id="actions",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="search",
label_key="storefront.actions.search",
icon="search",
route="",
order=10,
),
],
),
],
},
# Metrics provider for dashboard statistics
metrics_provider=_get_metrics_provider,

View File

@@ -13,7 +13,7 @@
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
<a href="{{ base_url }}storefront/products" class="hover:text-primary">Products</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium" x-text="categoryName">{{ category_slug | replace('-', ' ') | title if category_slug else 'Category' }}</span>
</div>
@@ -61,14 +61,14 @@
<div x-show="!loading && products.length > 0" class="product-grid">
<template x-for="product in products" :key="product.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden">
<a :href="`{{ base_url }}shop/products/${product.id}`">
<img loading="lazy" :src="product.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<a :href="`{{ base_url }}storefront/products/${product.id}`">
<img loading="lazy" :src="product.marketplace_product?.image_link || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="product.marketplace_product?.title"
class="w-full h-48 object-cover">
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${product.id}`" class="block">
<a :href="`{{ base_url }}storefront/products/${product.id}`" class="block">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="product.marketplace_product?.title"></h3>
</a>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2" x-text="product.marketplace_product?.description"></p>
@@ -99,7 +99,7 @@
<p class="text-gray-600 dark:text-gray-400 mb-4">
Check back later or browse other categories.
</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
<a href="{{ base_url }}storefront/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
Browse All Products
</a>
</div>
@@ -213,9 +213,9 @@ document.addEventListener('alpine:init', () => {
params.append('sort', this.sortBy);
}
console.log(`[SHOP] Loading category products from /api/v1/shop/products?${params}`);
console.log(`[SHOP] Loading category products from /api/v1/storefront/products?${params}`);
const response = await fetch(`/api/v1/shop/products?${params}`);
const response = await fetch(`/api/v1/storefront/products?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -246,7 +246,7 @@ document.addEventListener('alpine:init', () => {
console.log('[SHOP] Adding to cart:', product);
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
const payload = {
product_id: product.id,
quantity: 1

View File

@@ -12,7 +12,7 @@
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
<a href="{{ base_url }}storefront/products" class="hover:text-primary">Products</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium" x-text="product?.marketplace_product?.title || 'Product'">Product</span>
</div>
@@ -29,8 +29,8 @@
<div class="product-images">
<div class="main-image bg-white dark:bg-gray-800 rounded-lg overflow-hidden mb-4">
<img
:src="selectedImage || '/static/shop/img/placeholder.svg'"
@error="selectedImage = '/static/shop/img/placeholder.svg'"
:src="selectedImage || '/static/storefront/img/placeholder.svg'"
@error="selectedImage = '/static/storefront/img/placeholder.svg'"
:alt="product?.marketplace_product?.title"
class="w-full h-auto object-contain"
style="max-height: 600px;"
@@ -186,16 +186,16 @@
<div class="product-grid">
<template x-for="related in relatedProducts" :key="related.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer">
<a :href="`{{ base_url }}shop/products/${related.id}`">
<a :href="`{{ base_url }}storefront/products/${related.id}`">
<img
:src="related.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
:src="related.marketplace_product?.image_link || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="related.marketplace_product?.title"
class="w-full h-48 object-cover"
>
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${related.id}`">
<a :href="`{{ base_url }}storefront/products/${related.id}`">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="related.marketplace_product?.title"></h3>
</a>
<p class="text-2xl font-bold text-primary">
@@ -276,7 +276,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log(`[SHOP] Loading product ${this.productId}...`);
const response = await fetch(`/api/v1/shop/products/${this.productId}`);
const response = await fetch(`/api/v1/storefront/products/${this.productId}`);
if (!response.ok) {
throw new Error('Product not found');
@@ -301,7 +301,7 @@ document.addEventListener('alpine:init', () => {
this.showToast('Failed to load product', 'error');
// Redirect back to products after error
setTimeout(() => {
window.location.href = '{{ base_url }}shop/products';
window.location.href = '{{ base_url }}storefront/products';
}, 2000);
} finally {
this.loading = false;
@@ -311,7 +311,7 @@ document.addEventListener('alpine:init', () => {
// Load related products
async loadRelatedProducts() {
try {
const response = await fetch(`/api/v1/shop/products?limit=4`);
const response = await fetch(`/api/v1/storefront/products?limit=4`);
if (response.ok) {
const data = await response.json();
@@ -368,7 +368,7 @@ document.addEventListener('alpine:init', () => {
this.addingToCart = true;
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
const payload = {
product_id: parseInt(this.productId),
quantity: this.quantity

View File

@@ -73,14 +73,14 @@
<div x-show="!loading && products.length > 0" class="product-grid">
<template x-for="product in products" :key="product.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden">
<a :href="`{{ base_url }}shop/products/${product.id}`">
<img loading="lazy" :src="product.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<a :href="`{{ base_url }}storefront/products/${product.id}`">
<img loading="lazy" :src="product.marketplace_product?.image_link || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="product.marketplace_product?.title"
class="w-full h-48 object-cover">
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${product.id}`" class="block">
<a :href="`{{ base_url }}storefront/products/${product.id}`" class="block">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="product.marketplace_product?.title"></h3>
</a>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2" x-text="product.marketplace_product?.description"></p>
@@ -178,9 +178,9 @@ document.addEventListener('alpine:init', () => {
params.append('search', this.filters.search);
}
console.log(`[SHOP] Loading products from /api/v1/shop/products?${params}`);
console.log(`[SHOP] Loading products from /api/v1/storefront/products?${params}`);
const response = await fetch(`/api/v1/shop/products?${params}`);
const response = await fetch(`/api/v1/storefront/products?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -211,7 +211,7 @@ document.addEventListener('alpine:init', () => {
console.log('[SHOP] Adding to cart:', product);
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
const payload = {
product_id: product.id,
quantity: 1

View File

@@ -80,14 +80,14 @@
<div x-show="!loading && query && products.length > 0" class="product-grid">
<template x-for="product in products" :key="product.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden">
<a :href="`{{ base_url }}shop/products/${product.id}`">
<img loading="lazy" :src="product.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<a :href="`{{ base_url }}storefront/products/${product.id}`">
<img loading="lazy" :src="product.marketplace_product?.image_link || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="product.marketplace_product?.title"
class="w-full h-48 object-cover">
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${product.id}`" class="block">
<a :href="`{{ base_url }}storefront/products/${product.id}`" class="block">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="product.marketplace_product?.title"></h3>
</a>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2" x-text="product.marketplace_product?.description"></p>
@@ -254,9 +254,9 @@ document.addEventListener('alpine:init', () => {
limit: this.perPage
});
console.log(`[SHOP] Searching: /api/v1/shop/products/search?${params}`);
console.log(`[SHOP] Searching: /api/v1/storefront/products/search?${params}`);
const response = await fetch(`/api/v1/shop/products/search?${params}`);
const response = await fetch(`/api/v1/storefront/products/search?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -292,7 +292,7 @@ document.addEventListener('alpine:init', () => {
console.log('[SHOP] Adding to cart:', product);
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
const payload = {
product_id: product.id,
quantity: 1

View File

@@ -13,7 +13,7 @@
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">Account</a>
<a href="{{ base_url }}storefront/account/dashboard" class="hover:text-primary">Account</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium">Wishlist</span>
</div>
@@ -46,7 +46,7 @@
<p class="text-gray-600 dark:text-gray-400 mb-6">
Log in to your account to view and manage your wishlist.
</p>
<a href="{{ base_url }}shop/account/login" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
<a href="{{ base_url }}storefront/account/login" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
Log In
</a>
</div>
@@ -64,14 +64,14 @@
<span x-html="$icon('heart', 'w-5 h-5 text-red-500 fill-current')"></span>
</button>
<a :href="`{{ base_url }}shop/products/${item.product.id}`">
<img loading="lazy" :src="item.product.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<a :href="`{{ base_url }}storefront/products/${item.product.id}`">
<img loading="lazy" :src="item.product.marketplace_product?.image_link || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="item.product.marketplace_product?.title"
class="w-full h-48 object-cover">
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${item.product.id}`" class="block">
<a :href="`{{ base_url }}storefront/products/${item.product.id}`" class="block">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="item.product.marketplace_product?.title"></h3>
</a>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2" x-text="item.product.marketplace_product?.description"></p>
@@ -122,7 +122,7 @@
<p class="text-gray-600 dark:text-gray-400 mb-6">
Save items you like by clicking the heart icon on product pages.
</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
<a href="{{ base_url }}storefront/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
Browse Products
</a>
</div>
@@ -157,7 +157,7 @@ document.addEventListener('alpine:init', () => {
async checkLoginStatus() {
try {
const response = await fetch('/api/v1/shop/customers/me');
const response = await fetch('/api/v1/storefront/customers/me');
return response.ok;
} catch (error) {
return false;
@@ -170,7 +170,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log('[SHOP] Loading wishlist...');
const response = await fetch('/api/v1/shop/wishlist');
const response = await fetch('/api/v1/storefront/wishlist');
if (!response.ok) {
if (response.status === 401) {
@@ -197,7 +197,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log('[SHOP] Removing from wishlist:', item);
const response = await fetch(`/api/v1/shop/wishlist/${item.id}`, {
const response = await fetch(`/api/v1/storefront/wishlist/${item.id}`, {
method: 'DELETE'
});
@@ -217,7 +217,7 @@ document.addEventListener('alpine:init', () => {
console.log('[SHOP] Adding to cart:', product);
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
const payload = {
product_id: product.id,
quantity: 1

View File

@@ -11,10 +11,10 @@
{# Breadcrumbs #}
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<li><a href="{{ base_url }}shop/" class="hover:text-primary">Home</a></li>
<li><a href="{{ base_url }}storefront/" class="hover:text-primary">Home</a></li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
<a href="{{ base_url }}shop/cart" class="hover:text-primary">Cart</a>
<a href="{{ base_url }}storefront/cart" class="hover:text-primary">Cart</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
@@ -36,7 +36,7 @@
<span class="mx-auto h-12 w-12 text-gray-400 mb-4 block" x-html="$icon('shopping-bag', 'h-12 w-12 mx-auto')"></span>
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">Your cart is empty</h3>
<p class="text-gray-500 dark:text-gray-400 mb-6">Add some products before checking out.</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 text-white rounded-lg transition-colors" style="background-color: var(--color-primary)">
<a href="{{ base_url }}storefront/products" class="inline-block px-6 py-3 text-white rounded-lg transition-colors" style="background-color: var(--color-primary)">
Browse Products
</a>
</div>
@@ -384,8 +384,8 @@
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="item in cartItems" :key="item.product_id">
<div class="py-4 flex items-center gap-4">
<img loading="lazy" :src="item.image_url || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<img loading="lazy" :src="item.image_url || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
class="w-16 h-16 object-cover rounded-lg">
<div class="flex-1 min-w-0">
<p class="font-medium text-gray-900 dark:text-white truncate" x-text="item.name"></p>
@@ -428,8 +428,8 @@
<template x-for="item in cartItems" :key="item.product_id">
<div class="flex items-center gap-3">
<div class="relative">
<img loading="lazy" :src="item.image_url || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<img loading="lazy" :src="item.image_url || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
class="w-12 h-12 object-cover rounded">
<span class="absolute -top-2 -right-2 w-5 h-5 bg-gray-500 text-white text-xs rounded-full flex items-center justify-center" x-text="item.quantity"></span>
</div>
@@ -611,7 +611,7 @@ function checkoutPage() {
async loadCustomerData() {
try {
const response = await fetch('/api/v1/shop/auth/me');
const response = await fetch('/api/v1/storefront/auth/me');
if (response.ok) {
const data = await response.json();
this.isLoggedIn = true;
@@ -639,7 +639,7 @@ function checkoutPage() {
async loadSavedAddresses() {
try {
const response = await fetch('/api/v1/shop/addresses');
const response = await fetch('/api/v1/storefront/addresses');
if (response.ok) {
const data = await response.json();
this.savedAddresses = data.addresses || [];
@@ -706,7 +706,7 @@ function checkoutPage() {
async loadCart() {
this.loading = true;
try {
const response = await fetch(`/api/v1/shop/cart/${this.sessionId}`);
const response = await fetch(`/api/v1/storefront/cart/${this.sessionId}`);
if (response.ok) {
const data = await response.json();
this.cartItems = data.items || [];
@@ -790,7 +790,7 @@ function checkoutPage() {
country_iso: this.shippingAddress.country_iso,
is_default: this.shippingAddresses.length === 0 // Make default if first address
};
const response = await fetch('/api/v1/shop/addresses', {
const response = await fetch('/api/v1/storefront/addresses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(addressData)
@@ -820,7 +820,7 @@ function checkoutPage() {
country_iso: this.billingAddress.country_iso,
is_default: this.billingAddresses.length === 0 // Make default if first address
};
const response = await fetch('/api/v1/shop/addresses', {
const response = await fetch('/api/v1/storefront/addresses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(addressData)
@@ -883,7 +883,7 @@ function checkoutPage() {
console.log('[CHECKOUT] Placing order:', orderData);
const response = await fetch('/api/v1/shop/orders', {
const response = await fetch('/api/v1/storefront/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -900,7 +900,7 @@ function checkoutPage() {
console.log('[CHECKOUT] Order placed:', order.order_number);
// Redirect to confirmation page
window.location.href = '{{ base_url }}shop/order-confirmation?order=' + order.order_number;
window.location.href = '{{ base_url }}storefront/order-confirmation?order=' + order.order_number;
} catch (error) {
console.error('[CHECKOUT] Error placing order:', error);

View File

@@ -172,7 +172,7 @@
</template>
<!-- Preview button -->
<a
:href="`/stores/${storeCode}/shop/${page.slug}`"
:href="`/stores/${storeCode}/storefront/${page.slug}`"
target="_blank"
class="flex items-center justify-center p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 transition-colors"
title="Preview"
@@ -278,7 +278,7 @@
<span x-html="$icon('edit', 'w-5 h-5')"></span>
</a>
<a
:href="`/stores/${storeCode}/shop/${page.slug}`"
:href="`/stores/${storeCode}/storefront/${page.slug}`"
target="_blank"
class="flex items-center justify-center p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 transition-colors"
title="Preview"

View File

@@ -1,7 +1,7 @@
{# app/templates/store/landing-default.html #}
{# standalone #}
{# Default/Minimal Landing Page Template #}
{% extends "shop/base.html" %}
{% extends "storefront/base.html" %}
{% block title %}{{ store.name }}{% endblock %}
{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %}
@@ -36,7 +36,7 @@
{# CTA Button #}
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ base_url }}shop/"
<a href="{{ base_url }}storefront/"
class="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-lg text-white bg-primary hover:bg-primary-dark transition-colors shadow-lg hover:shadow-xl"
style="background-color: var(--color-primary)">
Browse Our Shop
@@ -71,7 +71,7 @@
Explore
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<a href="{{ base_url }}shop/products"
<a href="{{ base_url }}storefront/products"
class="block p-8 bg-white dark:bg-gray-900 rounded-lg shadow-md hover:shadow-xl transition-shadow text-center group">
<div class="text-4xl mb-4">🛍️</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2 group-hover:text-primary">
@@ -84,7 +84,7 @@
{% if header_pages %}
{% for page in header_pages[:2] %}
<a href="{{ base_url }}shop/{{ page.slug }}"
<a href="{{ base_url }}storefront/{{ page.slug }}"
class="block p-8 bg-white dark:bg-gray-900 rounded-lg shadow-md hover:shadow-xl transition-shadow text-center group">
<div class="text-4xl mb-4">📄</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2 group-hover:text-primary">
@@ -96,7 +96,7 @@
</a>
{% endfor %}
{% else %}
<a href="{{ base_url }}shop/about"
<a href="{{ base_url }}storefront/about"
class="block p-8 bg-white dark:bg-gray-900 rounded-lg shadow-md hover:shadow-xl transition-shadow text-center group">
<div class="text-4xl mb-4"></div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2 group-hover:text-primary">
@@ -107,7 +107,7 @@
</p>
</a>
<a href="{{ base_url }}shop/contact"
<a href="{{ base_url }}storefront/contact"
class="block p-8 bg-white dark:bg-gray-900 rounded-lg shadow-md hover:shadow-xl transition-shadow text-center group">
<div class="text-4xl mb-4">📧</div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2 group-hover:text-primary">

View File

@@ -1,7 +1,7 @@
{# app/templates/store/landing-full.html #}
{# standalone #}
{# Full Landing Page Template - Maximum Features #}
{% extends "shop/base.html" %}
{% extends "storefront/base.html" %}
{% block title %}{{ store.name }}{% endblock %}
{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %}
@@ -43,7 +43,7 @@
{% endif %}
<div class="flex flex-col sm:flex-row gap-4">
<a href="{{ base_url }}shop/"
<a href="{{ base_url }}storefront/"
class="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-lg text-white bg-primary hover:bg-primary-dark transition-colors shadow-lg"
style="background-color: var(--color-primary)">
Shop Now
@@ -174,7 +174,7 @@
Explore More
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<a href="{{ base_url }}shop/products"
<a href="{{ base_url }}storefront/products"
class="group relative overflow-hidden rounded-2xl bg-gradient-to-br from-primary/10 to-primary/5 dark:from-primary/20 dark:to-primary/10 p-8 hover:shadow-xl transition-all">
<div class="relative z-10">
<div class="text-5xl mb-4">🛍️</div>
@@ -190,7 +190,7 @@
{% if header_pages %}
{% for page in header_pages[:2] %}
<a href="{{ base_url }}shop/{{ page.slug }}"
<a href="{{ base_url }}storefront/{{ page.slug }}"
class="group relative overflow-hidden rounded-2xl bg-gradient-to-br from-accent/10 to-accent/5 dark:from-accent/20 dark:to-accent/10 p-8 hover:shadow-xl transition-all">
<div class="relative z-10">
<div class="text-5xl mb-4">📄</div>
@@ -205,7 +205,7 @@
</a>
{% endfor %}
{% else %}
<a href="{{ base_url }}shop/about"
<a href="{{ base_url }}storefront/about"
class="group relative overflow-hidden rounded-2xl bg-gradient-to-br from-accent/10 to-accent/5 dark:from-accent/20 dark:to-accent/10 p-8 hover:shadow-xl transition-all">
<div class="relative z-10">
<div class="text-5xl mb-4"></div>
@@ -219,7 +219,7 @@
<div class="absolute inset-0 bg-accent opacity-0 group-hover:opacity-5 transition-opacity"></div>
</a>
<a href="{{ base_url }}shop/contact"
<a href="{{ base_url }}storefront/contact"
class="group relative overflow-hidden rounded-2xl bg-gradient-to-br from-primary/10 to-primary/5 dark:from-primary/20 dark:to-primary/10 p-8 hover:shadow-xl transition-all">
<div class="relative z-10">
<div class="text-5xl mb-4">📧</div>
@@ -246,7 +246,7 @@
<p class="text-xl mb-10 opacity-90">
Join thousands of satisfied customers today
</p>
<a href="{{ base_url }}shop/products"
<a href="{{ base_url }}storefront/products"
class="inline-flex items-center justify-center px-10 py-5 text-lg font-bold rounded-xl text-primary bg-white hover:bg-gray-50 transition-all transform hover:scale-105 shadow-2xl">
View All Products
<span class="w-5 h-5 ml-2" x-html="$icon('arrow-right', 'w-5 h-5')"></span>

View File

@@ -1,7 +1,7 @@
{# app/templates/store/landing-minimal.html #}
{# standalone #}
{# Minimal Landing Page Template - Ultra Clean #}
{% extends "shop/base.html" %}
{% extends "storefront/base.html" %}
{% block title %}{{ store.name }}{% endblock %}
{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %}
@@ -37,7 +37,7 @@
{# Single CTA #}
<div>
<a href="{{ base_url }}shop/"
<a href="{{ base_url }}storefront/"
class="inline-flex items-center justify-center px-10 py-5 text-xl font-semibold rounded-full text-white bg-primary hover:bg-primary-dark transition-all transform hover:scale-105 shadow-2xl"
style="background-color: var(--color-primary)">
Enter Shop
@@ -49,11 +49,11 @@
{% if header_pages or footer_pages %}
<div class="mt-16 pt-8 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-wrap justify-center gap-6 text-sm">
<a href="{{ base_url }}shop/products" class="text-gray-600 dark:text-gray-400 hover:text-primary transition-colors">
<a href="{{ base_url }}storefront/products" class="text-gray-600 dark:text-gray-400 hover:text-primary transition-colors">
Products
</a>
{% for page in (header_pages or footer_pages)[:4] %}
<a href="{{ base_url }}shop/{{ page.slug }}" class="text-gray-600 dark:text-gray-400 hover:text-primary transition-colors">
<a href="{{ base_url }}storefront/{{ page.slug }}" class="text-gray-600 dark:text-gray-400 hover:text-primary transition-colors">
{{ page.title }}
</a>
{% endfor %}

View File

@@ -1,7 +1,7 @@
{# app/templates/store/landing-modern.html #}
{# standalone #}
{# Modern Landing Page Template - Feature Rich #}
{% extends "shop/base.html" %}
{% extends "storefront/base.html" %}
{% block title %}{{ store.name }}{% endblock %}
{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %}
@@ -40,7 +40,7 @@
{# CTAs #}
<div class="flex flex-col sm:flex-row gap-6 justify-center animate-fade-in animation-delay-400">
<a href="{{ base_url }}shop/"
<a href="{{ base_url }}storefront/"
class="group inline-flex items-center justify-center px-10 py-5 text-lg font-bold rounded-xl text-white bg-primary hover:bg-primary-dark transition-all transform hover:scale-105 shadow-2xl hover:shadow-3xl"
style="background-color: var(--color-primary)">
<span>Start Shopping</span>
@@ -137,7 +137,7 @@
<p class="text-xl mb-10 opacity-90">
Explore our collection and find what you're looking for
</p>
<a href="{{ base_url }}shop/products"
<a href="{{ base_url }}storefront/products"
class="inline-flex items-center justify-center px-10 py-5 text-lg font-bold rounded-xl text-primary bg-white hover:bg-gray-50 transition-all transform hover:scale-105 shadow-2xl">
Browse Products
<span class="w-5 h-5 ml-2" x-html="$icon('chevron-right', 'w-5 h-5')"></span>

View File

@@ -146,6 +146,29 @@ def get_context_for_frontend(
f"[CONTEXT] {len(modules_with_providers)} modules have providers but none contributed"
)
# Pass enabled module codes to templates for conditional rendering
context["enabled_modules"] = enabled_module_codes
# For storefront, build nav menu structure from module declarations
if frontend_type == FrontendType.STOREFRONT:
from app.modules.core.services.menu_discovery_service import (
menu_discovery_service,
)
platform_id = platform.id if platform else None
sections = menu_discovery_service.get_menu_sections_for_frontend(
db, FrontendType.STOREFRONT, platform_id
)
# Build dict of section_id -> list of enabled items for easy template access
storefront_nav: dict[str, list] = {}
for section in sections:
enabled_items = [
item for item in section.items if item.is_module_enabled
]
if enabled_items:
storefront_nav[section.id] = enabled_items
context["storefront_nav"] = storefront_nav
# Add any extra context passed by the caller
if extra_context:
context.update(extra_context)
@@ -318,6 +341,10 @@ def get_storefront_context(
)
base_url = f"{full_prefix}{store.subdomain}/"
# Read subscription info set by StorefrontAccessMiddleware
subscription = getattr(request.state, "subscription", None)
subscription_tier = getattr(request.state, "subscription_tier", None)
# Build storefront-specific base context
storefront_base = {
"store": store,
@@ -325,6 +352,11 @@ def get_storefront_context(
"clean_path": clean_path,
"access_method": access_method,
"base_url": base_url,
"enabled_modules": set(),
"storefront_nav": {},
"subscription": subscription,
"subscription_tier": subscription_tier,
"tier_code": subscription_tier.code if subscription_tier else None,
}
# If no db session, return just the base context

View File

@@ -132,6 +132,43 @@ customers_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="account",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="dashboard",
label_key="storefront.account.dashboard",
icon="home",
route="storefront/account/dashboard",
order=10,
),
MenuItemDefinition(
id="profile",
label_key="storefront.account.profile",
icon="user",
route="storefront/account/profile",
order=20,
),
MenuItemDefinition(
id="addresses",
label_key="storefront.account.addresses",
icon="map-pin",
route="storefront/account/addresses",
order=30,
),
MenuItemDefinition(
id="settings",
label_key="storefront.account.settings",
icon="cog",
route="storefront/account/settings",
order=90,
),
],
),
],
},
is_core=True, # Customers is a core module - customer data is fundamental
# =========================================================================

View File

@@ -392,7 +392,7 @@ function addressesPage() {
try {
const token = localStorage.getItem('customer_token');
const response = await fetch('/api/v1/shop/addresses', {
const response = await fetch('/api/v1/storefront/addresses', {
headers: {
'Authorization': token ? `Bearer ${token}` : '',
}
@@ -400,7 +400,7 @@ function addressesPage() {
if (!response.ok) {
if (response.status === 401) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
throw new Error('Failed to load addresses');
@@ -461,8 +461,8 @@ function addressesPage() {
try {
const token = localStorage.getItem('customer_token');
const url = this.editingAddress
? `/api/v1/shop/addresses/${this.editingAddress.id}`
: '/api/v1/shop/addresses';
? `/api/v1/storefront/addresses/${this.editingAddress.id}`
: '/api/v1/storefront/addresses';
const method = this.editingAddress ? 'PUT' : 'POST';
const response = await fetch(url, {
@@ -500,7 +500,7 @@ function addressesPage() {
try {
const token = localStorage.getItem('customer_token');
const response = await fetch(`/api/v1/shop/addresses/${this.deletingAddressId}`, {
const response = await fetch(`/api/v1/storefront/addresses/${this.deletingAddressId}`, {
method: 'DELETE',
headers: {
'Authorization': token ? `Bearer ${token}` : '',
@@ -525,7 +525,7 @@ function addressesPage() {
async setAsDefault(addressId) {
try {
const token = localStorage.getItem('customer_token');
const response = await fetch(`/api/v1/shop/addresses/${addressId}/default`, {
const response = await fetch(`/api/v1/storefront/addresses/${addressId}/default`, {
method: 'PUT',
headers: {
'Authorization': token ? `Bearer ${token}` : '',

View File

@@ -18,7 +18,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- Orders Card -->
<a href="{{ base_url }}shop/account/orders"
<a href="{{ base_url }}storefront/account/orders"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
@@ -36,7 +36,7 @@
</a>
<!-- Profile Card -->
<a href="{{ base_url }}shop/account/profile"
<a href="{{ base_url }}storefront/account/profile"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
@@ -53,7 +53,7 @@
</a>
<!-- Addresses Card -->
<a href="{{ base_url }}shop/account/addresses"
<a href="{{ base_url }}storefront/account/addresses"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
@@ -67,10 +67,10 @@
</a>
<!-- Messages Card -->
<a href="{{ base_url }}shop/account/messages"
<a href="{{ base_url }}storefront/account/messages"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700"
x-data="{ unreadCount: 0 }"
x-init="fetch('/api/v1/shop/messages/unread-count').then(r => r.json()).then(d => unreadCount = d.unread_count).catch(() => {})">
x-init="fetch('/api/v1/storefront/messages/unread-count').then(r => r.json()).then(d => unreadCount = d.unread_count).catch(() => {})">
<div class="flex items-center mb-4">
<div class="flex-shrink-0 relative">
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('chat-bubble-left', 'h-8 w-8')"></span>
@@ -141,7 +141,7 @@ function accountDashboard() {
// Close modal
this.showLogoutModal = false;
fetch('/api/v1/shop/auth/logout', {
fetch('/api/v1/storefront/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -157,14 +157,14 @@ function accountDashboard() {
// Redirect to login page
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
}, 500);
} else {
console.error('Logout failed with status:', response.status);
this.showToast('Logout failed', 'error');
// Still redirect on failure (cookie might be deleted)
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
}, 1000);
}
})
@@ -173,7 +173,7 @@ function accountDashboard() {
this.showToast('Logout failed', 'error');
// Redirect anyway
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
}, 1000);
});
}

View File

@@ -39,7 +39,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -143,13 +143,13 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
href="{{ base_url }}storefront/account/login">
Sign in
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
href="{{ base_url }}storefront/">
← Continue shopping
</a>
</p>
@@ -218,7 +218,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/forgot-password', {
const response = await fetch('/api/v1/storefront/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -38,7 +38,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -127,7 +127,7 @@
style="color: var(--color-primary);">
<span class="ml-2 text-gray-700 dark:text-gray-400">Remember me</span>
</label>
<a href="{{ base_url }}shop/account/forgot-password"
<a href="{{ base_url }}storefront/account/forgot-password"
class="text-sm font-medium hover:underline"
style="color: var(--color-primary);">
Forgot password?
@@ -150,13 +150,13 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Don't have an account?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/register">
href="{{ base_url }}storefront/account/register">
Create an account
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
href="{{ base_url }}storefront/">
← Continue shopping
</a>
</p>
@@ -238,7 +238,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/login', {
const response = await fetch('/api/v1/storefront/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -263,7 +263,7 @@
// Redirect to account page or return URL
setTimeout(() => {
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}shop/account';
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}storefront/account';
window.location.href = returnUrl;
}, 1000);

View File

@@ -11,7 +11,7 @@
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">My Account</a>
<a href="{{ base_url }}storefront/account/dashboard" class="hover:text-primary">My Account</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
@@ -330,11 +330,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
const response = await fetch('/api/v1/shop/profile', {
const response = await fetch('/api/v1/storefront/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -344,7 +344,7 @@ function shopProfilePage() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
throw new Error('Failed to load profile');
@@ -380,11 +380,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
const response = await fetch('/api/v1/shop/profile', {
const response = await fetch('/api/v1/storefront/profile', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
@@ -429,11 +429,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
const response = await fetch('/api/v1/shop/profile', {
const response = await fetch('/api/v1/storefront/profile', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
@@ -472,11 +472,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
const response = await fetch('/api/v1/shop/profile/password', {
const response = await fetch('/api/v1/storefront/profile/password', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,

View File

@@ -39,7 +39,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -218,7 +218,7 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Already have an account?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
href="{{ base_url }}storefront/account/login">
Sign in instead
</a>
</p>
@@ -341,7 +341,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/register', {
const response = await fetch('/api/v1/storefront/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -360,7 +360,7 @@
// Redirect to login after 2 seconds
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login?registered=true';
window.location.href = '{{ base_url }}storefront/account/login?registered=true';
}, 2000);
} catch (error) {

View File

@@ -39,7 +39,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -80,7 +80,7 @@
Please request a new password reset link.
</p>
<a href="{{ base_url }}shop/account/forgot-password"
<a href="{{ base_url }}storefront/account/forgot-password"
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
Request New Link
</a>
@@ -164,7 +164,7 @@
You can now sign in with your new password.
</p>
<a href="{{ base_url }}shop/account/login"
<a href="{{ base_url }}storefront/account/login"
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
Sign In
</a>
@@ -177,13 +177,13 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
href="{{ base_url }}storefront/account/login">
Sign in
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
href="{{ base_url }}storefront/">
← Continue shopping
</a>
</p>
@@ -273,7 +273,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/reset-password', {
const response = await fetch('/api/v1/storefront/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -168,6 +168,22 @@ loyalty_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="account",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="loyalty",
label_key="storefront.account.loyalty",
icon="gift",
route="storefront/account/loyalty",
order=60,
),
],
),
],
},
is_core=False, # Loyalty can be disabled
# =========================================================================

View File

@@ -9,7 +9,7 @@
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="mb-8">
<a href="{{ base_url }}shop/account/dashboard" class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400 hover:text-primary mb-4">
<a href="{{ base_url }}storefront/account/dashboard" class="inline-flex items-center text-sm text-gray-600 dark:text-gray-400 hover:text-primary mb-4">
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
Back to Account
</a>
@@ -26,7 +26,7 @@
<span x-html="$icon('gift', 'w-16 h-16 mx-auto text-gray-300 dark:text-gray-600')"></span>
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Join Our Rewards Program!</h2>
<p class="mt-2 text-gray-600 dark:text-gray-400">Earn points on every purchase and redeem for rewards.</p>
<a href="{{ base_url }}shop/loyalty/join"
<a href="{{ base_url }}storefront/loyalty/join"
class="mt-6 inline-flex items-center px-6 py-3 text-sm font-medium text-white rounded-lg"
style="background-color: var(--color-primary)">
<span x-html="$icon('plus', 'w-5 h-5 mr-2')"></span>
@@ -125,7 +125,7 @@
<div>
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Recent Activity</h2>
<a href="{{ base_url }}shop/account/loyalty/history"
<a href="{{ base_url }}storefront/account/loyalty/history"
class="text-sm font-medium hover:underline" style="color: var(--color-primary)">
View All
</a>

View File

@@ -62,12 +62,12 @@
<!-- Actions -->
<div class="space-y-3">
<a href="{{ base_url }}shop/account/loyalty"
<a href="{{ base_url }}storefront/account/loyalty"
class="block w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors text-center"
style="background-color: var(--color-primary)">
View My Loyalty Dashboard
</a>
<a href="{{ base_url }}shop"
<a href="{{ base_url }}storefront"
class="block w-full py-3 px-4 text-gray-700 dark:text-gray-300 font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-center">
Continue Shopping
</a>

View File

@@ -173,6 +173,22 @@ messaging_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="account",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="messages",
label_key="storefront.account.messages",
icon="chat-bubble-left-right",
route="storefront/account/messages",
order=50,
),
],
),
],
},
is_core=True, # Core module - email/notifications required for registration, password reset, etc.
# =========================================================================

View File

@@ -13,7 +13,7 @@
<nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<li class="inline-flex items-center">
<a href="{{ base_url }}shop/account/dashboard"
<a href="{{ base_url }}storefront/account/dashboard"
class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary dark:text-gray-400 dark:hover:text-white"
style="--hover-color: var(--color-primary)">
<span class="w-4 h-4 mr-2" x-html="$icon('home', 'w-4 h-4')"></span>
@@ -292,7 +292,7 @@ function shopMessages() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
@@ -304,7 +304,7 @@ function shopMessages() {
params.append('status', this.statusFilter);
}
const response = await fetch(`/api/v1/shop/messages?${params}`, {
const response = await fetch(`/api/v1/storefront/messages?${params}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -313,7 +313,7 @@ function shopMessages() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
throw new Error('Failed to load conversations');
@@ -335,11 +335,11 @@ function shopMessages() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
const response = await fetch(`/api/v1/shop/messages/${conversationId}`, {
const response = await fetch(`/api/v1/storefront/messages/${conversationId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -357,7 +357,7 @@ function shopMessages() {
});
// Update URL without reload
const url = `{{ base_url }}shop/account/messages/${conversationId}`;
const url = `{{ base_url }}storefront/account/messages/${conversationId}`;
history.pushState({}, '', url);
} catch (error) {
console.error('Error loading conversation:', error);
@@ -372,7 +372,7 @@ function shopMessages() {
const token = localStorage.getItem('customer_token');
if (!token) return;
const response = await fetch(`/api/v1/shop/messages/${this.selectedConversation.id}`, {
const response = await fetch(`/api/v1/storefront/messages/${this.selectedConversation.id}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -404,7 +404,7 @@ function shopMessages() {
this.loadConversations();
// Update URL
history.pushState({}, '', '{{ base_url }}shop/account/messages');
history.pushState({}, '', '{{ base_url }}storefront/account/messages');
},
async sendReply() {
@@ -415,7 +415,7 @@ function shopMessages() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
@@ -425,7 +425,7 @@ function shopMessages() {
formData.append('attachments', file);
}
const response = await fetch(`/api/v1/shop/messages/${this.selectedConversation.id}/messages`, {
const response = await fetch(`/api/v1/storefront/messages/${this.selectedConversation.id}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`

View File

@@ -137,6 +137,22 @@ orders_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="account",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="orders",
label_key="storefront.account.orders",
icon="clipboard-list",
route="storefront/account/orders",
order=40,
),
],
),
],
},
is_core=False,
# =========================================================================

View File

@@ -11,11 +11,11 @@
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">My Account</a>
<a href="{{ base_url }}storefront/account/dashboard" class="hover:text-primary">My Account</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
<a href="{{ base_url }}shop/account/orders" class="hover:text-primary">Orders</a>
<a href="{{ base_url }}storefront/account/orders" class="hover:text-primary">Orders</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
@@ -36,7 +36,7 @@
<div class="ml-3">
<h3 class="text-lg font-medium text-red-800 dark:text-red-200">Error loading order</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-300" x-text="error"></p>
<a href="{{ base_url }}shop/account/orders"
<a href="{{ base_url }}storefront/account/orders"
class="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700">
Back to Orders
</a>
@@ -314,7 +314,7 @@
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
If you have any questions about your order, please contact us.
</p>
<a href="{{ base_url }}shop/account/messages"
<a href="{{ base_url }}storefront/account/messages"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors"
style="background-color: var(--color-primary)">
<span class="h-4 w-4 mr-2" x-html="$icon('chat-bubble-left', 'h-4 w-4')"></span>
@@ -326,7 +326,7 @@
<!-- Back Button -->
<div class="mt-8">
<a href="{{ base_url }}shop/account/orders"
<a href="{{ base_url }}storefront/account/orders"
class="inline-flex items-center text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-primary">
<span class="h-4 w-4 mr-2" x-html="$icon('chevron-left', 'h-4 w-4')"></span>
Back to Orders
@@ -375,11 +375,11 @@ function shopOrderDetailPage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
const response = await fetch(`/api/v1/shop/orders/${this.orderId}`, {
const response = await fetch(`/api/v1/storefront/orders/${this.orderId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -389,7 +389,7 @@ function shopOrderDetailPage() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
if (response.status === 404) {
@@ -501,11 +501,11 @@ function shopOrderDetailPage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
const response = await fetch(`/api/v1/shop/orders/${this.orderId}/invoice`, {
const response = await fetch(`/api/v1/storefront/orders/${this.orderId}/invoice`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
@@ -516,7 +516,7 @@ function shopOrderDetailPage() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
if (response.status === 404) {

View File

@@ -11,7 +11,7 @@
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">My Account</a>
<a href="{{ base_url }}storefront/account/dashboard" class="hover:text-primary">My Account</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
@@ -45,7 +45,7 @@
<span class="mx-auto h-12 w-12 text-gray-400 block" x-html="$icon('shopping-bag', 'h-12 w-12 mx-auto')"></span>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No orders yet</h3>
<p class="mt-2 text-gray-500 dark:text-gray-400">Start shopping to see your orders here.</p>
<a href="{{ base_url }}shop/products"
<a href="{{ base_url }}storefront/products"
class="mt-6 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark transition-colors"
style="background-color: var(--color-primary)">
Browse Products
@@ -79,7 +79,7 @@
<span x-text="getStatusLabel(order.status)"></span>
</span>
<!-- View Details Button -->
<a :href="'{{ base_url }}shop/account/orders/' + order.id"
<a :href="'{{ base_url }}storefront/account/orders/' + order.id"
class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
View Details
<span class="ml-2 h-4 w-4" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
@@ -167,12 +167,12 @@ function shopOrdersPage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
const skip = (page - 1) * this.perPage;
const response = await fetch(`/api/v1/shop/orders?skip=${skip}&limit=${this.perPage}`, {
const response = await fetch(`/api/v1/storefront/orders?skip=${skip}&limit=${this.perPage}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -182,7 +182,7 @@ function shopOrdersPage() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
throw new Error('Failed to load orders');