feat(storefront): homepage, module gating, widget protocol, i18n fixes
Some checks failed
Some checks failed
Storefront homepage & module gating:
- CMS owns storefront GET / (slug="home" with 3-tier resolution)
- Catalog loses GET / (keeps /products only)
- Store root redirect (GET / → /store/dashboard or /store/login)
- Route gating: non-core modules return 404 when disabled for platform
- Seed store default homepages per platform
Widget protocol for customer dashboard:
- StorefrontDashboardCard contract in widgets.py
- Widget aggregator get_storefront_dashboard_cards()
- Orders and Loyalty module widget providers
- Dashboard template renders contributed cards (no module names)
Landing template module-agnostic:
- CTAs driven by storefront_nav (not hardcoded module names)
- Header actions check nav item IDs (not enabled_modules)
- Remove hardcoded "Add Product" sidebar button
- Remove all enabled_modules checks from storefront templates
i18n fixes:
- Title placeholder resolution ({{store_name}}) for store default pages
- Storefront nav label_keys prefixed with module code
- Add storefront.account.* keys to 6 modules (en/fr/de/lb)
- Header/footer CMS pages use get_translated_title(current_language)
- Footer labels use i18n keys instead of hardcoded English
Icon cleanup:
- Standardize on map-pin (remove location-marker alias)
- Replace all location-marker references across templates and docs
Docs:
- Storefront builder vision proposal (6 phases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,28 +141,28 @@ customers_module = ModuleDefinition(
|
||||
items=[
|
||||
MenuItemDefinition(
|
||||
id="dashboard",
|
||||
label_key="storefront.account.dashboard",
|
||||
label_key="customers.storefront.account.dashboard",
|
||||
icon="home",
|
||||
route="account/dashboard",
|
||||
order=10,
|
||||
),
|
||||
MenuItemDefinition(
|
||||
id="profile",
|
||||
label_key="storefront.account.profile",
|
||||
label_key="customers.storefront.account.profile",
|
||||
icon="user",
|
||||
route="account/profile",
|
||||
order=20,
|
||||
),
|
||||
MenuItemDefinition(
|
||||
id="addresses",
|
||||
label_key="storefront.account.addresses",
|
||||
label_key="customers.storefront.account.addresses",
|
||||
icon="map-pin",
|
||||
route="account/addresses",
|
||||
order=30,
|
||||
),
|
||||
MenuItemDefinition(
|
||||
id="settings",
|
||||
label_key="storefront.account.settings",
|
||||
label_key="customers.storefront.account.settings",
|
||||
icon="cog",
|
||||
route="account/settings",
|
||||
order=90,
|
||||
|
||||
@@ -52,5 +52,13 @@
|
||||
"customers_delete_desc": "Kundendatensätze entfernen",
|
||||
"customers_export": "Kunden exportieren",
|
||||
"customers_export_desc": "Kundendaten exportieren"
|
||||
},
|
||||
"storefront": {
|
||||
"account": {
|
||||
"dashboard": "Dashboard",
|
||||
"profile": "Profil",
|
||||
"addresses": "Adressen",
|
||||
"settings": "Einstellungen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,13 @@
|
||||
"customers_section": "Customers",
|
||||
"customers": "Customers",
|
||||
"all_customers": "All Customers"
|
||||
},
|
||||
"storefront": {
|
||||
"account": {
|
||||
"dashboard": "Dashboard",
|
||||
"profile": "Profile",
|
||||
"addresses": "Addresses",
|
||||
"settings": "Settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,13 @@
|
||||
"customers_delete_desc": "Supprimer les fiches clients",
|
||||
"customers_export": "Exporter les clients",
|
||||
"customers_export_desc": "Exporter les données clients"
|
||||
},
|
||||
"storefront": {
|
||||
"account": {
|
||||
"dashboard": "Tableau de bord",
|
||||
"profile": "Profil",
|
||||
"addresses": "Adresses",
|
||||
"settings": "Paramètres"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,13 @@
|
||||
"customers_delete_desc": "Clientedossieren ewechhuelen",
|
||||
"customers_export": "Clienten exportéieren",
|
||||
"customers_export_desc": "Clientedaten exportéieren"
|
||||
},
|
||||
"storefront": {
|
||||
"account": {
|
||||
"dashboard": "Dashboard",
|
||||
"profile": "Profil",
|
||||
"addresses": "Adressen",
|
||||
"settings": "Astellungen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,9 +195,25 @@ async def shop_account_dashboard_page(
|
||||
},
|
||||
)
|
||||
|
||||
# Collect dashboard cards from enabled modules via widget protocol
|
||||
from app.modules.core.services.widget_aggregator import widget_aggregator
|
||||
|
||||
store = getattr(request.state, "store", None)
|
||||
platform = getattr(request.state, "platform", None)
|
||||
dashboard_cards = []
|
||||
if store and platform:
|
||||
dashboard_cards = widget_aggregator.get_storefront_dashboard_cards(
|
||||
db,
|
||||
store_id=store.id,
|
||||
customer_id=current_customer.id,
|
||||
platform_id=platform.id,
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"customers/storefront/dashboard.html",
|
||||
get_storefront_context(request, db=db, user=current_customer),
|
||||
get_storefront_context(
|
||||
request, db=db, user=current_customer, dashboard_cards=dashboard_cards
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<!-- Empty State -->
|
||||
<div x-show="!loading && !error && addresses.length === 0"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||
<span class="mx-auto h-12 w-12 text-gray-400 block" x-html="$icon('location-marker', 'h-12 w-12 mx-auto')"></span>
|
||||
<span class="mx-auto h-12 w-12 text-gray-400 block" x-html="$icon('map-pin', 'h-12 w-12 mx-auto')"></span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No addresses yet</h3>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Add your first address to speed up checkout.</p>
|
||||
<button @click="openAddModal()"
|
||||
|
||||
@@ -17,25 +17,31 @@
|
||||
<!-- Dashboard Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
|
||||
<!-- Orders Card -->
|
||||
<a href="{{ base_url }}account/orders"
|
||||
{# Module-contributed cards (orders, loyalty, etc.) — rendered via widget protocol #}
|
||||
{% for card in dashboard_cards|default([]) %}
|
||||
<a href="{{ base_url }}{{ card.route }}"
|
||||
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">
|
||||
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('shopping-bag', 'h-8 w-8')"></span>
|
||||
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('{{ card.icon }}', 'h-8 w-8')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Orders</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">View order history</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ card.title }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ card.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if card.value is not none %}
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-primary" style="color: var(--color-primary)">{{ user.total_orders }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Total Orders</p>
|
||||
<p class="text-2xl font-bold text-primary" style="color: var(--color-primary)">{{ card.value }}</p>
|
||||
{% if card.value_label %}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ card.value_label }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Profile Card -->
|
||||
<!-- Profile Card (always shown — core) -->
|
||||
<a href="{{ base_url }}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">
|
||||
@@ -52,12 +58,12 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Addresses Card -->
|
||||
<!-- Addresses Card (always shown — core) -->
|
||||
<a href="{{ base_url }}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">
|
||||
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('location-marker', 'h-8 w-8')"></span>
|
||||
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('map-pin', 'h-8 w-8')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Addresses</h3>
|
||||
@@ -66,36 +72,7 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{% if 'loyalty' in enabled_modules %}
|
||||
<!-- Loyalty Rewards Card -->
|
||||
<a href="{{ base_url }}account/loyalty"
|
||||
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="{ points: null, loaded: false }"
|
||||
x-init="fetch('/api/v1/storefront/loyalty/card').then(r => r.json()).then(d => { if (d.card) { points = d.card.points_balance; } loaded = true; }).catch(() => { loaded = true; })">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('gift', 'h-8 w-8')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Loyalty Rewards</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">View your points & rewards</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<template x-if="loaded && points !== null">
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-primary" style="color: var(--color-primary)" x-text="points.toLocaleString()"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Points Balance</p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="loaded && points === null">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Join our rewards program</p>
|
||||
</template>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Messages Card -->
|
||||
<!-- Messages Card (always shown — messaging is core) -->
|
||||
<a href="{{ base_url }}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 }"
|
||||
@@ -126,10 +103,6 @@
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Since</p>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Orders</p>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.total_orders }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Number</p>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
|
||||
|
||||
Reference in New Issue
Block a user