feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
CI / ruff (push) Successful in 12s
CI / pytest (push) Successful in 50m57s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 40s
CI / deploy (push) Successful in 51s

- Fix platform-grouped merchant sidebar menu with core items at root level
- Add merchant store management (detail page, create store, team page)
- Fix store settings 500 error by removing dead stripe/API tab
- Move onboarding translations to module-owned locale files
- Fix onboarding banner i18n with server-side rendering + context inheritance
- Refactor login language selectors to use languageSelector() function (LANG-002)
- Move HTTPException handling to global exception handler in merchant routes (API-003)
- Add language selector to all login pages and portal headers
- Fix customer module: drop order stats from customer model, add to orders module
- Fix admin menu config visibility for super admin platform context
- Fix storefront auth and layout issues
- Add missing i18n translations for onboarding steps (en/fr/de/lb)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 23:48:25 +01:00
parent f141cc4e6a
commit a77a8a3a98
113 changed files with 3741 additions and 2923 deletions

View File

@@ -29,7 +29,7 @@
{{ error_state('Error loading customers') }}
<!-- Stats Cards -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Card: Total Customers -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
@@ -59,36 +59,6 @@
</p>
</div>
</div>
<!-- Card: With Orders -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('shopping-bag', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
With Orders
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.with_orders || 0">
0
</p>
</div>
</div>
<!-- Card: Total Spent -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
<span x-html="$icon('currency-euro', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Total Revenue
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatCurrency(stats.total_spent || 0)">
0
</p>
</div>
</div>
</div>
<!-- Search and Filters -->
@@ -134,8 +104,6 @@
<th class="px-4 py-3">Customer</th>
<th class="px-4 py-3">Store</th>
<th class="px-4 py-3">Customer #</th>
<th class="px-4 py-3">Orders</th>
<th class="px-4 py-3">Total Spent</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Joined</th>
<th class="px-4 py-3">Actions</th>
@@ -145,7 +113,7 @@
<!-- Loading state -->
<template x-if="loadingCustomers && customers.length === 0">
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2')"></span>
<p>Loading customers...</p>
</td>
@@ -155,7 +123,7 @@
<!-- Empty state -->
<template x-if="!loadingCustomers && customers.length === 0">
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('user-group', 'w-12 h-12 mx-auto mb-2 text-gray-300')"></span>
<p class="font-medium">No customers found</p>
<p class="text-sm mt-1">Try adjusting your search or filters</p>
@@ -189,16 +157,6 @@
<span class="font-mono text-xs" x-text="customer.customer_number"></span>
</td>
<!-- Orders -->
<td class="px-4 py-3 text-sm">
<span x-text="customer.total_orders || 0"></span>
</td>
<!-- Total Spent -->
<td class="px-4 py-3 text-sm">
<span x-text="formatCurrency(customer.total_spent || 0)"></span>
</td>
<!-- Status -->
<td class="px-4 py-3 text-xs">
<span

View File

@@ -0,0 +1,178 @@
{# app/templates/store/customer-detail.html #}
{% extends "store/base.html" %}
{% from 'shared/macros/headers.html' import page_header_flex %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% block title %}Customer Details{% endblock %}
{% block alpine_data %}storeCustomerDetail(){% endblock %}
{% block content %}
<!-- Back Button -->
<div class="mb-6">
<a :href="`/store/${storeCode}/customers`"
class="inline-flex items-center text-sm text-gray-600 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400 mb-4">
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-1')"></span>
Back to Customers
</a>
</div>
{% call page_header_flex(title='Customer Details', subtitle='View customer profile and order history') %}
<div class="flex items-center gap-2" x-show="!loading && customer">
<span
class="px-3 py-1 text-sm font-semibold rounded-full"
:class="customer?.is_active
? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'
: 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="customer?.is_active ? 'Active' : 'Inactive'"
></span>
</div>
{% endcall %}
{{ loading_state('Loading customer details...') }}
{{ error_state('Error loading customer') }}
<!-- Main Content -->
<div x-show="!loading && !error && customer" class="grid gap-6 lg:grid-cols-3">
<!-- Left Column: Profile -->
<div class="lg:col-span-2 space-y-6">
<!-- Profile Card -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Profile</h3>
</div>
<div class="p-6">
<div class="flex items-center mb-6">
<div class="w-16 h-16 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-4">
<span class="text-xl font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials()"></span>
</div>
<div>
<p class="text-lg font-semibold text-gray-800 dark:text-gray-200" x-text="customerName"></p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="customer?.email"></p>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase">Customer #</p>
<p class="text-sm font-mono font-medium text-gray-700 dark:text-gray-200" x-text="customer?.customer_number || '-'"></p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase">Phone</p>
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="customer?.phone || '-'"></p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase">Joined</p>
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="formatDate(customer?.created_at)"></p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase">Language</p>
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="(customer?.preferred_language || 'Default').toUpperCase()"></p>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase">Marketing</p>
<p class="text-sm font-medium" :class="customer?.marketing_consent ? 'text-green-600' : 'text-gray-500'" x-text="customer?.marketing_consent ? 'Opted in' : 'Opted out'"></p>
</div>
</div>
</div>
</div>
<!-- Recent Orders -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Recent Orders</h3>
<a :href="`/store/${storeCode}/orders?customer_id=${customerId}`"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300"
x-show="recentOrders.length > 0">
View All Orders
<span x-html="$icon('arrow-right', 'w-4 h-4 inline ml-1')"></span>
</a>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="order in recentOrders" :key="order.id">
<a :href="`/store/${storeCode}/orders/${order.id}`" class="flex items-center justify-between p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div>
<p class="font-mono font-semibold text-gray-800 dark:text-gray-200" x-text="order.order_number || `#${order.id}`"></p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="formatDate(order.created_at || order.order_date)"></p>
</div>
<div class="text-right">
<p class="font-semibold text-gray-800 dark:text-gray-200" x-text="formatPrice(order.total_amount_cents)"></p>
<span
class="text-xs px-2 py-1 font-semibold leading-tight rounded-full"
:class="{
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': order.status === 'delivered' || order.status === 'completed',
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': order.status === 'pending',
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': order.status === 'processing' || order.status === 'shipped',
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': order.status === 'cancelled',
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': !['delivered','completed','pending','processing','shipped','cancelled'].includes(order.status)
}"
x-text="order.status"
></span>
</div>
</a>
</template>
<div x-show="recentOrders.length === 0" class="p-6 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('shopping-bag', 'w-8 h-8 mx-auto mb-2 text-gray-300')"></span>
<p>No orders yet</p>
</div>
</div>
</div>
</div>
<!-- Right Column: Order Stats & Actions -->
<div class="space-y-6">
<!-- Order Stats Cards -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Order Statistics</h3>
</div>
<div class="p-6 space-y-4">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500 dark:text-gray-400">Total Orders</span>
<span class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="orderStats.total_orders || 0"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500 dark:text-gray-400">Total Spent</span>
<span class="text-lg font-semibold text-purple-600 dark:text-purple-400" x-text="formatPrice(orderStats.total_spent_cents || 0)"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500 dark:text-gray-400">Avg Order Value</span>
<span class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatPrice(orderStats.total_orders ? Math.round(orderStats.total_spent_cents / orderStats.total_orders) : 0)"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500 dark:text-gray-400">Last Order</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="orderStats.last_order_date ? formatDate(orderStats.last_order_date) : 'Never'"></span>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Actions</h3>
</div>
<div class="p-4 space-y-2">
<a :href="`/store/${storeCode}/orders?customer_id=${customerId}`"
class="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-300 dark:hover:bg-purple-800">
<span x-html="$icon('shopping-bag', 'w-4 h-4 mr-2')"></span>
View All Orders
</a>
<button
@click="messageCustomer()"
class="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-green-600 bg-green-100 rounded-lg hover:bg-green-200 dark:bg-green-900 dark:text-green-300 dark:hover:bg-green-800">
<span x-html="$icon('chat-bubble-left-right', 'w-4 h-4 mr-2')"></span>
Send Message
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
window.customerDetailData = {
customerId: {{ customer_id }}
};
</script>
<script defer src="{{ url_for('customers_static', path='store/js/customer-detail.js') }}"></script>
{% endblock %}

View File

@@ -107,7 +107,6 @@
<th class="px-4 py-3">Customer</th>
<th class="px-4 py-3">Email</th>
<th class="px-4 py-3">Joined</th>
<th class="px-4 py-3">Orders</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
@@ -130,8 +129,6 @@
<td class="px-4 py-3 text-sm" x-text="customer.email || '-'"></td>
<!-- Joined -->
<td class="px-4 py-3 text-sm" x-text="formatDate(customer.created_at)"></td>
<!-- Orders -->
<td class="px-4 py-3 text-sm font-semibold" x-text="customer.order_count || 0"></td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
@@ -142,13 +139,6 @@
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</button>
<button
@click="viewCustomerOrders(customer)"
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
title="View Orders"
>
<span x-html="$icon('document-text', 'w-5 h-5')"></span>
</button>
<button
@click="messageCustomer(customer)"
class="p-1 text-gray-500 hover:text-green-600 dark:text-gray-400 dark:hover:text-green-400"
@@ -162,7 +152,7 @@
</template>
<!-- Empty State -->
<tr x-show="customers.length === 0">
<td colspan="5" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="4" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('users', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
<p class="text-lg font-medium">No customers found</p>
@@ -199,12 +189,12 @@
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="formatDate(selectedCustomer?.created_at)"></p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Total Orders</p>
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="selectedCustomer?.order_count || 0"></p>
<p class="text-gray-500 dark:text-gray-400">Customer #</p>
<p class="font-medium font-mono text-gray-800 dark:text-gray-200" x-text="selectedCustomer?.customer_number || '-'"></p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Total Spent</p>
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="formatPrice(selectedCustomer?.total_spent || 0)"></p>
<p class="text-gray-500 dark:text-gray-400">Status</p>
<p class="font-medium" :class="selectedCustomer?.is_active ? 'text-green-600' : 'text-red-600'" x-text="selectedCustomer?.is_active ? 'Active' : 'Inactive'"></p>
</div>
</div>
</div>
@@ -212,55 +202,11 @@
<button @click="showDetailModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
Close
</button>
<button @click="messageCustomer(selectedCustomer); showDetailModal = false" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
Send Message
</button>
<a :href="`/store/${storeCode}/customers/${selectedCustomer?.id}`" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
View Full Profile
</a>
</div>
{% endcall %}
<!-- Customer Orders Modal -->
<div x-show="showOrdersModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
<div class="w-full max-w-2xl bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="showOrdersModal = false">
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">
Orders for <span x-text="`${selectedCustomer?.first_name || ''} ${selectedCustomer?.last_name || ''}`.trim()"></span>
</h3>
<button @click="showOrdersModal = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<div class="p-4 max-h-96 overflow-y-auto">
<template x-if="customerOrders.length === 0">
<p class="text-center text-gray-500 dark:text-gray-400 py-8">No orders found for this customer</p>
</template>
<template x-for="order in customerOrders" :key="order.id">
<div class="flex items-center justify-between p-3 border-b dark:border-gray-700 last:border-0">
<div>
<p class="font-mono font-semibold text-gray-800 dark:text-gray-200" x-text="order.order_number || `#${order.id}`"></p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="formatDate(order.created_at)"></p>
</div>
<div class="text-right">
<p class="font-semibold text-gray-800 dark:text-gray-200" x-text="formatPrice(order.total)"></p>
<span
class="text-xs px-2 py-1 font-semibold leading-tight rounded-full"
:class="{
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': order.status === 'completed',
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': order.status === 'pending',
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': order.status === 'processing'
}"
x-text="order.status"
></span>
</div>
</div>
</template>
</div>
<div class="flex justify-end p-4 border-t dark:border-gray-700">
<button @click="showOrdersModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
Close
</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}

View File

@@ -1,11 +1,11 @@
{# app/templates/storefront/account/forgot-password.html #}
{# standalone #}
<!DOCTYPE html>
<html :class="{ 'dark': dark }" x-data="forgotPassword()" lang="en">
<html :class="{ 'dark': dark }" x-data="forgotPassword()" lang="{{ current_language|default('fr') }}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forgot Password - {{ store.name }}</title>
<title>{{ _("auth.forgot_password") }} - {{ store.name }}</title>
<!-- Fonts: Local fallback + Google Fonts -->
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
@@ -57,7 +57,6 @@
<div class="text-6xl mb-4">🔐</div>
{% endif %}
<h2 class="text-2xl font-bold text-white mb-2">{{ store.name }}</h2>
<p class="text-white opacity-90">Reset your password</p>
</div>
</div>
@@ -68,11 +67,11 @@
<template x-if="!emailSent">
<div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Forgot Password
{{ _("auth.reset_password") }}
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
Enter your email address and we'll send you a link to reset your password.
{{ _("auth.reset_password_desc") }}
</p>
<!-- Error Message -->
@@ -84,14 +83,14 @@
<!-- Forgot Password Form -->
<form @submit.prevent="handleSubmit">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Email Address</span>
<span class="text-gray-700 dark:text-gray-400">{{ _("common.email") }}</span>
<input x-model="email"
:disabled="loading"
@input="clearErrors"
type="email"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.email }"
placeholder="your@email.com"
placeholder="{{ _('auth.email_placeholder') }}"
autocomplete="email"
required />
<span x-show="errors.email" x-text="errors.email"
@@ -100,10 +99,13 @@
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Send Reset Link</span>
<span x-show="!loading">{{ _("auth.send_reset_link") }}</span>
<span x-show="loading" class="flex items-center justify-center">
<span class="inline w-4 h-4 mr-2" x-html="$icon('spinner', 'w-4 h-4 animate-spin')"></span>
Sending...
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ _("auth.sending") }}
</span>
</button>
</form>
@@ -114,24 +116,25 @@
<template x-if="emailSent">
<div class="text-center">
<div class="flex items-center justify-center w-16 h-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900">
<span class="w-8 h-8 text-green-600 dark:text-green-400" x-html="$icon('check', 'w-8 h-8')"></span>
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Check Your Email
{{ _("auth.check_email") }}
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
We've sent a password reset link to <strong x-text="email"></strong>.
Please check your inbox and click the link to reset your password.
{{ _("auth.reset_link_sent") }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
Didn't receive the email? Check your spam folder or
{{ _("auth.didnt_receive_email") }}
<button @click="emailSent = false"
class="font-medium hover:underline"
style="color: var(--color-primary);">
try again
{{ _("auth.try_again") }}
</button>
</p>
</div>
@@ -140,19 +143,34 @@
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _("auth.remember_password") }}</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}account/login">
Sign in
{{ _("auth.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 }}">
← Continue shopping
&larr; {{ _("auth.continue_shopping") }}
</a>
</p>
<!-- Language selector -->
<div class="flex items-center justify-center gap-2 mt-6"
x-data='languageSelector("{{ request.state.language|default("fr") }}", {{ ["en", "fr", "de", "lb"]|tojson }})'>
<template x-for="lang in languages" :key="lang">
<button
@click="setLanguage(lang)"
class="px-2.5 py-1 text-sm font-semibold rounded-md transition-all"
:class="currentLang === lang
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 ring-1 ring-purple-300 dark:ring-purple-700'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
x-text="lang.toUpperCase()"
></button>
</template>
</div>
</div>
</div>
</div>
@@ -164,6 +182,22 @@
<!-- Forgot Password Logic -->
<script>
function languageSelector(currentLang, enabledLanguages) {
return {
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
async setLanguage(lang) {
if (lang === this.currentLang) return;
await fetch('/api/v1/platform/language/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: lang }),
});
window.location.reload();
},
};
}
function forgotPassword() {
return {
// Data

View File

@@ -64,7 +64,7 @@
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Customer Login
{{ _("auth.customer_login") }}
</h1>
<!-- Success Message (after registration) -->
@@ -82,14 +82,14 @@
<!-- Login Form -->
<form @submit.prevent="handleLogin">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Email Address</span>
<span class="text-gray-700 dark:text-gray-400">{{ _("common.email") }}</span>
<input x-model="credentials.email"
:disabled="loading"
@input="clearAllErrors"
type="email"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.email }"
placeholder="your@email.com"
placeholder="{{ _('auth.email_placeholder') }}"
autocomplete="email"
required />
<span x-show="errors.email" x-text="errors.email"
@@ -97,7 +97,7 @@
</label>
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Password</span>
<span class="text-gray-700 dark:text-gray-400">{{ _("auth.password") }}</span>
<div class="relative">
<input x-model="credentials.password"
:disabled="loading"
@@ -105,7 +105,7 @@
:type="showPassword ? 'text' : 'password'"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.password }"
placeholder="Enter your password"
placeholder="{{ _('auth.password_placeholder') }}"
autocomplete="current-password"
required />
<button type="button"
@@ -125,21 +125,21 @@
x-model="rememberMe"
class="form-checkbox focus-primary focus:outline-none"
style="color: var(--color-primary);">
<span class="ml-2 text-gray-700 dark:text-gray-400">Remember me</span>
<span class="ml-2 text-gray-700 dark:text-gray-400">{{ _("auth.remember_me") }}</span>
</label>
<a href="{{ base_url }}account/forgot-password"
class="text-sm font-medium hover:underline"
style="color: var(--color-primary);">
Forgot password?
{{ _("auth.forgot_password") }}
</a>
</div>
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Sign in</span>
<span x-show="!loading">{{ _("auth.sign_in") }}</span>
<span x-show="loading" class="flex items-center justify-center">
<span class="inline w-4 h-4 mr-2" x-html="$icon('spinner', 'w-4 h-4 animate-spin')"></span>
Signing in...
{{ _("auth.signing_in") }}
</span>
</button>
</form>
@@ -147,19 +147,34 @@
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Don't have an account?</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _("auth.no_account") }}</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}account/register">
Create an account
{{ _("auth.create_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 }}">
← Continue shopping
&larr; {{ _("auth.continue_shopping") }}
</a>
</p>
<!-- Language selector (always show all platform languages on login page) -->
<div class="flex items-center justify-center gap-2 mt-6"
x-data='languageSelector("{{ request.state.language|default("fr") }}", {{ ["en", "fr", "de", "lb"]|tojson }})'>
<template x-for="lang in languages" :key="lang">
<button
@click="setLanguage(lang)"
class="px-2.5 py-1 text-sm font-semibold rounded-md transition-all"
:class="currentLang === lang
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 ring-1 ring-purple-300 dark:ring-purple-700'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
x-text="lang.toUpperCase()"
></button>
</template>
</div>
</div>
</div>
</div>
@@ -171,6 +186,22 @@
<!-- Login Logic -->
<script>
function languageSelector(currentLang, enabledLanguages) {
return {
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
async setLanguage(lang) {
if (lang === this.currentLang) return;
await fetch('/api/v1/platform/language/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: lang }),
});
window.location.reload();
},
};
}
function customerLogin() {
return {
// Data

View File

@@ -65,7 +65,7 @@
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Create Account
{{ _("auth.create_account_title") }}
</h1>
<!-- Success Message -->
@@ -85,7 +85,7 @@
<!-- First Name -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
First Name <span class="text-red-600">*</span>
{{ _("auth.first_name") }} <span class="text-red-600">*</span>
</span>
<input x-model="formData.first_name"
:disabled="loading"
@@ -102,7 +102,7 @@
<!-- Last Name -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Last Name <span class="text-red-600">*</span>
{{ _("auth.last_name") }} <span class="text-red-600">*</span>
</span>
<input x-model="formData.last_name"
:disabled="loading"
@@ -119,7 +119,7 @@
<!-- Email -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Email Address <span class="text-red-600">*</span>
{{ _("common.email") }} <span class="text-red-600">*</span>
</span>
<input x-model="formData.email"
:disabled="loading"
@@ -136,7 +136,7 @@
<!-- Phone (Optional) -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Phone Number</span>
<span class="text-gray-700 dark:text-gray-400">{{ _("auth.phone_number") }}</span>
<input x-model="formData.phone"
:disabled="loading"
type="tel"
@@ -147,7 +147,7 @@
<!-- Password -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Password <span class="text-red-600">*</span>
{{ _("auth.password") }} <span class="text-red-600">*</span>
</span>
<div class="relative">
<input x-model="formData.password"
@@ -166,7 +166,7 @@
</button>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Must contain at least 8 characters, one letter, and one number
{{ _("auth.password_requirements") }}
</p>
<span x-show="errors.password" x-text="errors.password"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
@@ -175,7 +175,7 @@
<!-- Confirm Password -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Confirm Password <span class="text-red-600">*</span>
{{ _("auth.confirm_password") }} <span class="text-red-600">*</span>
</span>
<input x-model="confirmPassword"
:disabled="loading"
@@ -198,16 +198,16 @@
class="form-checkbox focus-primary focus:outline-none mt-1"
style="color: var(--color-primary);">
<label for="marketingConsent" class="ml-2 text-sm text-gray-600 dark:text-gray-400">
I'd like to receive news and special offers
{{ _("auth.marketing_consent") }}
</label>
</div>
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-6 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Create Account</span>
<span x-show="!loading">{{ _("auth.create_account_title") }}</span>
<span x-show="loading" class="flex items-center justify-center">
<span class="inline w-4 h-4 mr-2" x-html="$icon('spinner', 'w-4 h-4 animate-spin')"></span>
Creating account...
{{ _("auth.creating_account") }}
</span>
</button>
</form>
@@ -215,13 +215,28 @@
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Already have an account?</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ _("auth.already_have_account") }}</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}account/login">
Sign in instead
{{ _("auth.sign_in_instead") }}
</a>
</p>
<!-- Language selector (always show all platform languages on login/register pages) -->
<div class="flex items-center justify-center gap-2 mt-6"
x-data='languageSelector("{{ request.state.language|default("fr") }}", {{ ["en", "fr", "de", "lb"]|tojson }})'>
<template x-for="lang in languages" :key="lang">
<button
@click="setLanguage(lang)"
class="px-2.5 py-1 text-sm font-semibold rounded-md transition-all"
:class="currentLang === lang
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 ring-1 ring-purple-300 dark:ring-purple-700'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
x-text="lang.toUpperCase()"
></button>
</template>
</div>
</div>
</div>
</div>
@@ -233,6 +248,22 @@
<!-- Registration Logic -->
<script>
function languageSelector(currentLang, enabledLanguages) {
return {
currentLang: currentLang || 'fr',
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
async setLanguage(lang) {
if (lang === this.currentLang) return;
await fetch('/api/v1/platform/language/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: lang }),
});
window.location.reload();
},
};
}
function customerRegistration() {
return {
// Data