fix(storefront): i18n sweep + locale-aware reset-password and welcome email
Some checks failed
Some checks failed
Test 5 (storefront password reset + customer dashboard) surfaced five
issues that all traced back to missing i18n plumbing:
- Forgot-password email arrived in EN regardless of storefront locale —
handler now prefers request.state.language over customer.preferred_language,
and loyalty self-enrollment backfills preferred_language for new + returning
customers so future locale-sensitive flows hit the right language without
being told twice.
- reset-password.html rendered "undefined" icon boxes because $icon magic
wasn't loaded in the standalone page — replaced with inline SVGs matching
the forgot-password.html convention.
- reset-password.html was hardcoded English: added lang attr, full _()
sweep (22 new auth.* keys × 4 locales), language selector, and JS
validation strings exposed via tojson.
- "Continue shopping" CTA renamed to "Back to Home" (auth.back_to_home,
4 locales) on login + forgot + reset — loyalty storefronts have no
catalog to continue to, mirroring the earlier enroll-success rename.
- /account dashboard, profile, addresses were hardcoded English in the
body (menu was FR because base layout uses _()). New customers.storefront
.pages.{dashboard,profile,addresses}.* namespace (~80 keys × 4 locales),
templates updated, Alpine JS strings injected via window.__*I18n.
18 files, 18 changed; arch validation: 126 warnings before = 126 after,
mkdocs --strict clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{# app/templates/storefront/account/addresses.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}My Addresses - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('customers.storefront.pages.addresses.title') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block alpine_data %}addressesPage(){% endblock %}
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<!-- Page Header -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Addresses</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Manage your shipping and billing addresses</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.addresses.title') }}</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.addresses.subtitle') }}</p>
|
||||
</div>
|
||||
<button @click="openAddModal()"
|
||||
class="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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
style="background-color: var(--color-primary)">
|
||||
<span class="-ml-1 mr-2 h-5 w-5" x-html="$icon('plus', 'h-5 w-5')"></span>
|
||||
Add Address
|
||||
{{ _('customers.storefront.pages.addresses.add_address') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
<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('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>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">{{ _('customers.storefront.pages.addresses.empty_state_title') }}</h3>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.addresses.empty_state_subtitle') }}</p>
|
||||
<button @click="openAddModal()"
|
||||
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"
|
||||
style="background-color: var(--color-primary)">
|
||||
Add Your First Address
|
||||
{{ _('customers.storefront.pages.addresses.add_first_address') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -56,14 +56,14 @@
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="address.address_type === 'shipping' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'">
|
||||
<span class="-ml-0.5 mr-1 h-3 w-3" x-html="$icon('check-circle', 'h-3 w-3')"></span>
|
||||
Default <span x-text="address.address_type === 'shipping' ? 'Shipping' : 'Billing'" class="ml-1"></span>
|
||||
<span x-text="address.address_type === 'shipping' ? i18n.defaultShipping : i18n.defaultBilling"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Address Type Badge (non-default) -->
|
||||
<div x-show="!address.is_default" class="absolute top-4 right-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
|
||||
x-text="address.address_type === 'shipping' ? 'Shipping' : 'Billing'"></span>
|
||||
x-text="address.address_type === 'shipping' ? i18n.shipping : i18n.billing"></span>
|
||||
</div>
|
||||
|
||||
<!-- Address Content -->
|
||||
@@ -81,16 +81,16 @@
|
||||
<button @click="openEditModal(address)"
|
||||
class="text-sm font-medium text-primary hover:text-primary-dark"
|
||||
style="color: var(--color-primary)">
|
||||
Edit
|
||||
{{ _('common.edit') }}
|
||||
</button>
|
||||
<button x-show="!address.is_default"
|
||||
@click="setAsDefault(address.id)"
|
||||
class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
|
||||
Set as Default
|
||||
{{ _('customers.storefront.pages.addresses.set_default') }}
|
||||
</button>
|
||||
<button @click="openDeleteModal(address.id)"
|
||||
class="text-sm font-medium text-red-600 hover:text-red-700">
|
||||
Delete
|
||||
{{ _('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,28 +140,28 @@
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="w-full">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-6"
|
||||
x-text="editingAddress ? 'Edit Address' : 'Add New Address'"></h3>
|
||||
x-text="editingAddress ? i18n.editAddress : i18n.addNewAddress"></h3>
|
||||
|
||||
<form @submit.prevent="saveAddress()" class="space-y-4">
|
||||
<!-- Address Type -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address Type</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.address_type') }}</label>
|
||||
<select x-model="addressForm.address_type"
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
<option value="shipping">Shipping Address</option>
|
||||
<option value="billing">Billing Address</option>
|
||||
<option value="shipping">{{ _('customers.storefront.pages.addresses.shipping_address') }}</option>
|
||||
<option value="billing">{{ _('customers.storefront.pages.addresses.billing_address') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Name Row -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">First Name *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.first_name') }} *</label>
|
||||
<input type="text" x-model="addressForm.first_name" required
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Last Name *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.last_name') }} *</label>
|
||||
<input type="text" x-model="addressForm.last_name" required
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
@@ -169,21 +169,21 @@
|
||||
|
||||
<!-- Company -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Company (optional)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.company_optional') }}</label>
|
||||
<input type="text" x-model="addressForm.company"
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- Address Line 1 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.address_line_1') }} *</label>
|
||||
<input type="text" x-model="addressForm.address_line_1" required
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- Address Line 2 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Address Line 2 (optional)</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.address_line_2_optional') }}</label>
|
||||
<input type="text" x-model="addressForm.address_line_2"
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
@@ -191,12 +191,12 @@
|
||||
<!-- City & Postal Code -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Postal Code *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.postal_code') }} *</label>
|
||||
<input type="text" x-model="addressForm.postal_code" required
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">City *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.city') }} *</label>
|
||||
<input type="text" x-model="addressForm.city" required
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary focus:ring-primary sm:text-sm">
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@
|
||||
|
||||
<!-- Country -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Country *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('customers.storefront.pages.addresses.country') }} *</label>
|
||||
<select x-model="addressForm.country_iso"
|
||||
@change="addressForm.country_name = countries.find(c => c.iso === addressForm.country_iso)?.name || ''"
|
||||
required
|
||||
@@ -220,9 +220,8 @@
|
||||
<input type="checkbox" x-model="addressForm.is_default"
|
||||
class="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary"
|
||||
style="color: var(--color-primary)">
|
||||
<label class="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
Set as default <span x-text="addressForm.address_type === 'shipping' ? 'shipping' : 'billing'"></span> address
|
||||
</label>
|
||||
<label class="ml-2 text-sm text-gray-700 dark:text-gray-300"
|
||||
x-text="addressForm.address_type === 'shipping' ? i18n.setAsDefaultShipping : i18n.setAsDefaultBilling"></label>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
@@ -234,14 +233,14 @@
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button type="button" @click="showAddressModal = false"
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm 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">
|
||||
Cancel
|
||||
{{ _('common.cancel') }}
|
||||
</button>
|
||||
<button type="submit"
|
||||
:disabled="saving"
|
||||
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark disabled:opacity-50"
|
||||
style="background-color: var(--color-primary)">
|
||||
<span x-show="!saving" x-text="editingAddress ? 'Save Changes' : 'Add Address'"></span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
<span x-show="!saving" x-text="editingAddress ? i18n.saveChanges : i18n.addAddress"></span>
|
||||
<span x-show="saving">{{ _('customers.storefront.pages.addresses.saving') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -288,10 +287,10 @@
|
||||
<span class="h-6 w-6 text-red-600 dark:text-red-400" x-html="$icon('exclamation-triangle', 'h-6 w-6')"></span>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Delete Address</h3>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">{{ _('customers.storefront.pages.addresses.delete_address') }}</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Are you sure you want to delete this address? This action cannot be undone.
|
||||
{{ _('customers.storefront.pages.addresses.delete_confirm') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,12 +300,12 @@
|
||||
<button @click="confirmDelete()"
|
||||
:disabled="deleting"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50">
|
||||
<span x-show="!deleting">Delete</span>
|
||||
<span x-show="deleting">Deleting...</span>
|
||||
<span x-show="!deleting">{{ _('common.delete') }}</span>
|
||||
<span x-show="deleting">{{ _('customers.storefront.pages.addresses.deleting') }}</span>
|
||||
</button>
|
||||
<button @click="showDeleteModal = false"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 sm:mt-0 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
{{ _('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,9 +315,31 @@
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
window.__addressesPageI18n = {
|
||||
defaultShipping: {{ _('customers.storefront.pages.addresses.default_shipping')|tojson }},
|
||||
defaultBilling: {{ _('customers.storefront.pages.addresses.default_billing')|tojson }},
|
||||
shipping: {{ _('customers.storefront.pages.addresses.shipping')|tojson }},
|
||||
billing: {{ _('customers.storefront.pages.addresses.billing')|tojson }},
|
||||
editAddress: {{ _('customers.storefront.pages.addresses.edit_address')|tojson }},
|
||||
addNewAddress: {{ _('customers.storefront.pages.addresses.add_new_address')|tojson }},
|
||||
addAddress: {{ _('customers.storefront.pages.addresses.add_address')|tojson }},
|
||||
saveChanges: {{ _('customers.storefront.pages.addresses.save_changes')|tojson }},
|
||||
setAsDefaultShipping: {{ _('customers.storefront.pages.addresses.set_as_default_shipping')|tojson }},
|
||||
setAsDefaultBilling: {{ _('customers.storefront.pages.addresses.set_as_default_billing')|tojson }},
|
||||
addressUpdated: {{ _('customers.storefront.pages.addresses.address_updated')|tojson }},
|
||||
addressAdded: {{ _('customers.storefront.pages.addresses.address_added')|tojson }},
|
||||
addressDeleted: {{ _('customers.storefront.pages.addresses.address_deleted')|tojson }},
|
||||
defaultUpdated: {{ _('customers.storefront.pages.addresses.default_updated')|tojson }},
|
||||
failedToLoad: {{ _('customers.storefront.pages.addresses.failed_to_load')|tojson }},
|
||||
failedToSave: {{ _('customers.storefront.pages.addresses.failed_to_save')|tojson }},
|
||||
failedToDelete: {{ _('customers.storefront.pages.addresses.failed_to_delete')|tojson }},
|
||||
failedToSetDefault: {{ _('customers.storefront.pages.addresses.failed_to_set_default')|tojson }},
|
||||
};
|
||||
function addressesPage() {
|
||||
const i18n = window.__addressesPageI18n || {};
|
||||
return {
|
||||
...storefrontLayoutData(),
|
||||
i18n,
|
||||
|
||||
// State
|
||||
loading: true,
|
||||
@@ -403,14 +424,14 @@ function addressesPage() {
|
||||
window.location.href = '{{ base_url }}account/login';
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to load addresses');
|
||||
throw new Error(i18n.failedToLoad);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.addresses = data.addresses;
|
||||
} catch (err) {
|
||||
console.error('[ADDRESSES] Error loading:', err);
|
||||
this.error = 'Failed to load addresses. Please try again.';
|
||||
this.error = i18n.failedToLoad;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -476,15 +497,15 @@ function addressesPage() {
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.detail || data.message || 'Failed to save address');
|
||||
throw new Error(data.detail || data.message || i18n.failedToSave);
|
||||
}
|
||||
|
||||
this.showAddressModal = false;
|
||||
this.showToast(this.editingAddress ? 'Address updated' : 'Address added', 'success');
|
||||
this.showToast(this.editingAddress ? i18n.addressUpdated : i18n.addressAdded, 'success');
|
||||
await this.loadAddresses();
|
||||
} catch (err) {
|
||||
console.error('[ADDRESSES] Error saving:', err);
|
||||
this.formError = err.message || 'Failed to save address. Please try again.';
|
||||
this.formError = err.message || i18n.failedToSave;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -508,15 +529,15 @@ function addressesPage() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete address');
|
||||
throw new Error(i18n.failedToDelete);
|
||||
}
|
||||
|
||||
this.showDeleteModal = false;
|
||||
this.showToast('Address deleted', 'success');
|
||||
this.showToast(i18n.addressDeleted, 'success');
|
||||
await this.loadAddresses();
|
||||
} catch (err) {
|
||||
console.error('[ADDRESSES] Error deleting:', err);
|
||||
this.showToast('Failed to delete address', 'error');
|
||||
this.showToast(i18n.failedToDelete, 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
@@ -533,14 +554,14 @@ function addressesPage() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to set default address');
|
||||
throw new Error(i18n.failedToSetDefault);
|
||||
}
|
||||
|
||||
this.showToast('Default address updated', 'success');
|
||||
this.showToast(i18n.defaultUpdated, 'success');
|
||||
await this.loadAddresses();
|
||||
} catch (err) {
|
||||
console.error('[ADDRESSES] Error setting default:', err);
|
||||
this.showToast('Failed to set default address', 'error');
|
||||
this.showToast(i18n.failedToSetDefault, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% extends "storefront/base.html" %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}My Account - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('customers.storefront.pages.dashboard.title') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block alpine_data %}accountDashboard(){% endblock %}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Account</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Welcome back, {{ user.first_name }}!</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.title') }}</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.welcome_back', name=user.first_name) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Grid -->
|
||||
@@ -49,8 +49,8 @@
|
||||
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('user', 'h-8 w-8')"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Profile</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Edit your information</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.profile_card_title') }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.profile_card_subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -66,8 +66,8 @@
|
||||
<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>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Manage addresses</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.addresses_card_title') }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.addresses_card_subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -85,26 +85,27 @@
|
||||
x-text="unreadCount > 9 ? '9+' : unreadCount"></span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Messages</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Contact support</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.dashboard.messages_card_title') }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.messages_card_subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="unreadCount > 0">
|
||||
<p class="text-sm text-primary font-medium" style="color: var(--color-primary)" x-text="unreadCount + ' unread message' + (unreadCount > 1 ? 's' : '')"></p>
|
||||
<p class="text-sm text-primary font-medium" style="color: var(--color-primary)"
|
||||
x-text="(unreadCount === 1 ? {{ _('customers.storefront.pages.dashboard.unread_messages_singular')|tojson }} : {{ _('customers.storefront.pages.dashboard.unread_messages_plural')|tojson }}).replace('{count}', unreadCount)"></p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Account Summary -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Account Summary</h3>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">{{ _('customers.storefront.pages.dashboard.summary_title') }}</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Since</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">{{ _('customers.storefront.pages.dashboard.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">Customer Number</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">{{ _('customers.customer_number') }}</p>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +115,7 @@
|
||||
<div class="mt-8 flex justify-end">
|
||||
<button @click="showLogoutModal = true"
|
||||
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
||||
Logout
|
||||
{{ _('customers.storefront.pages.dashboard.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,19 +123,24 @@
|
||||
<!-- Logout Confirmation Modal -->
|
||||
{{ confirm_modal(
|
||||
id='logoutModal',
|
||||
title='Logout Confirmation',
|
||||
message="Are you sure you want to logout? You'll need to sign in again to access your account.",
|
||||
title=_('customers.storefront.pages.dashboard.logout_confirm_title'),
|
||||
message=_('customers.storefront.pages.dashboard.logout_confirm_message'),
|
||||
confirm_action='confirmLogout()',
|
||||
show_var='showLogoutModal',
|
||||
confirm_text='Logout',
|
||||
cancel_text='Cancel',
|
||||
confirm_text=_('customers.storefront.pages.dashboard.logout'),
|
||||
cancel_text=_('common.cancel'),
|
||||
variant='danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
window.__accountDashboardI18n = {
|
||||
logoutSuccess: {{ _('customers.storefront.pages.dashboard.logout_success')|tojson }},
|
||||
logoutFailed: {{ _('customers.storefront.pages.dashboard.logout_failed')|tojson }},
|
||||
};
|
||||
function accountDashboard() {
|
||||
const i18n = window.__accountDashboardI18n || {};
|
||||
return {
|
||||
...storefrontLayoutData(),
|
||||
showLogoutModal: false,
|
||||
@@ -155,7 +161,7 @@ function accountDashboard() {
|
||||
localStorage.removeItem('customer_token');
|
||||
|
||||
// Show success message
|
||||
this.showToast('Logged out successfully', 'success');
|
||||
this.showToast(i18n.logoutSuccess, 'success');
|
||||
|
||||
// Redirect to login page
|
||||
setTimeout(() => {
|
||||
@@ -163,7 +169,7 @@ function accountDashboard() {
|
||||
}, 500);
|
||||
} else {
|
||||
console.error('Logout failed with status:', response.status);
|
||||
this.showToast('Logout failed', 'error');
|
||||
this.showToast(i18n.logoutFailed, 'error');
|
||||
// Still redirect on failure (cookie might be deleted)
|
||||
setTimeout(() => {
|
||||
window.location.href = '{{ base_url }}account/login';
|
||||
@@ -172,7 +178,7 @@ function accountDashboard() {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout error:', error);
|
||||
this.showToast('Logout failed', 'error');
|
||||
this.showToast(i18n.logoutFailed, 'error');
|
||||
// Redirect anyway
|
||||
setTimeout(() => {
|
||||
window.location.href = '{{ base_url }}account/login';
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
<p class="mt-2 text-center">
|
||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||
href="{{ base_url }}">
|
||||
← {{ _("auth.continue_shopping") }}
|
||||
← {{ _("auth.back_to_home") }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -180,6 +180,15 @@
|
||||
<!-- Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
{# Translated client-side strings — kept in sync with auth.* keys above #}
|
||||
<script>
|
||||
window.__forgotPasswordI18n = {
|
||||
emailRequired: {{ _('auth.email_required')|tojson }},
|
||||
invalidEmail: {{ _('auth.invalid_email')|tojson }},
|
||||
forgotPasswordFailed: {{ _('auth.forgot_password_failed')|tojson }},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Forgot Password Logic -->
|
||||
<script>
|
||||
function languageSelector(currentLang, enabledLanguages) {
|
||||
@@ -199,6 +208,7 @@
|
||||
}
|
||||
|
||||
function forgotPassword() {
|
||||
const i18n = window.__forgotPasswordI18n || {};
|
||||
return {
|
||||
// Data
|
||||
email: '',
|
||||
@@ -240,12 +250,12 @@
|
||||
|
||||
// Basic validation
|
||||
if (!this.email) {
|
||||
this.errors.email = 'Email is required';
|
||||
this.errors.email = i18n.emailRequired;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)) {
|
||||
this.errors.email = 'Please enter a valid email address';
|
||||
this.errors.email = i18n.invalidEmail;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -265,7 +275,7 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.detail || 'Failed to send reset link');
|
||||
throw new Error(data.detail || i18n.forgotPasswordFailed);
|
||||
}
|
||||
|
||||
// Success - show email sent message
|
||||
@@ -273,7 +283,7 @@
|
||||
|
||||
} catch (error) {
|
||||
console.error('Forgot password error:', error);
|
||||
this.showAlert(error.message || 'Failed to send reset link. Please try again.');
|
||||
this.showAlert(error.message || i18n.forgotPasswordFailed);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
<p class="mt-2 text-center">
|
||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||
href="{{ base_url }}">
|
||||
← {{ _("auth.continue_shopping") }}
|
||||
← {{ _("auth.back_to_home") }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/templates/storefront/account/profile.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}My Profile - {{ store.name }}{% endblock %}
|
||||
{% block title %}{{ _('customers.storefront.pages.profile.title') }} - {{ store.name }}{% endblock %}
|
||||
|
||||
{% block alpine_data %}shopProfilePage(){% endblock %}
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
<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 }}account/dashboard" class="hover:text-primary">My Account</a>
|
||||
<a href="{{ base_url }}account/dashboard" class="hover:text-primary">{{ _('customers.storefront.pages.profile.breadcrumb_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>
|
||||
<span class="text-gray-900 dark:text-white">Profile</span>
|
||||
<span class="text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.breadcrumb_profile') }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Profile</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Manage your account information and preferences</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.title') }}</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.profile.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@@ -58,15 +58,15 @@
|
||||
<!-- Profile Information Section -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Profile Information</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Update your personal details</p>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.info_section_title') }}</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.info_section_subtitle') }}</p>
|
||||
</div>
|
||||
<form @submit.prevent="saveProfile" class="p-6 space-y-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<!-- First Name -->
|
||||
<div>
|
||||
<label for="first_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
First Name <span class="text-red-500">*</span>
|
||||
{{ _('customers.first_name') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="first_name" x-model="profileForm.first_name" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -78,7 +78,7 @@
|
||||
<!-- Last Name -->
|
||||
<div>
|
||||
<label for="last_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Last Name <span class="text-red-500">*</span>
|
||||
{{ _('customers.last_name') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="last_name" x-model="profileForm.last_name" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -91,7 +91,7 @@
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Email Address <span class="text-red-500">*</span>
|
||||
{{ _('customers.storefront.pages.profile.email_label') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="email" id="email" x-model="profileForm.email" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -103,7 +103,7 @@
|
||||
<!-- Phone -->
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Phone Number
|
||||
{{ _('auth.phone_number') }}
|
||||
</label>
|
||||
<input type="tel" id="phone" x-model="profileForm.phone"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -122,7 +122,7 @@
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style="background-color: var(--color-primary)">
|
||||
<span x-show="savingProfile" class="-ml-1 mr-2 h-4 w-4" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
|
||||
<span x-text="savingProfile ? 'Saving...' : 'Save Changes'"></span>
|
||||
<span x-text="savingProfile ? i18n.saving : i18n.saveChanges"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -131,25 +131,25 @@
|
||||
<!-- Preferences Section -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Preferences</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your account preferences</p>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('customers.storefront.pages.profile.prefs_section_title') }}</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.prefs_section_subtitle') }}</p>
|
||||
</div>
|
||||
<form @submit.prevent="savePreferences" class="p-6 space-y-6">
|
||||
<!-- Language -->
|
||||
<div>
|
||||
<label for="language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Preferred Language
|
||||
{{ _('customers.storefront.pages.profile.preferred_language') }}
|
||||
</label>
|
||||
<select id="language" x-model="preferencesForm.preferred_language"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
focus:ring-2 focus:ring-primary focus:border-transparent
|
||||
dark:bg-gray-700 dark:text-white"
|
||||
style="--tw-ring-color: var(--color-primary)">
|
||||
<option value="">Use shop default</option>
|
||||
<option value="">{{ _('customers.storefront.pages.profile.use_shop_default') }}</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Francais</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="lb">Letzebuergesch</option>
|
||||
<option value="lb">Lëtzebuergesch</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -164,10 +164,10 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label for="marketing_consent" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Marketing Communications
|
||||
{{ _('customers.storefront.pages.profile.marketing_communications') }}
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Receive emails about new products, offers, and promotions
|
||||
{{ _('customers.storefront.pages.profile.marketing_desc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +182,7 @@
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style="background-color: var(--color-primary)">
|
||||
<span x-show="savingPreferences" class="-ml-1 mr-2 h-4 w-4" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
|
||||
<span x-text="savingPreferences ? 'Saving...' : 'Save Preferences'"></span>
|
||||
<span x-text="savingPreferences ? i18n.saving : i18n.savePreferences"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -191,14 +191,14 @@
|
||||
<!-- Change Password Section -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Change Password</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Update your account password</p>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('auth.change_password') }}</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.change_password_subtitle') }}</p>
|
||||
</div>
|
||||
<form @submit.prevent="changePassword" class="p-6 space-y-6">
|
||||
<!-- Current Password -->
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Current Password <span class="text-red-500">*</span>
|
||||
{{ _('auth.current_password') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="current_password" x-model="passwordForm.current_password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -210,7 +210,7 @@
|
||||
<!-- New Password -->
|
||||
<div>
|
||||
<label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
New Password <span class="text-red-500">*</span>
|
||||
{{ _('auth.new_password') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="new_password" x-model="passwordForm.new_password" required
|
||||
minlength="8"
|
||||
@@ -219,14 +219,14 @@
|
||||
dark:bg-gray-700 dark:text-white"
|
||||
style="--tw-ring-color: var(--color-primary)">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 8 characters with at least one letter and one number
|
||||
{{ _('auth.password_requirements') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Confirm New Password <span class="text-red-500">*</span>
|
||||
{{ _('auth.confirm_password') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="confirm_password" x-model="passwordForm.confirm_password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -235,7 +235,7 @@
|
||||
style="--tw-ring-color: var(--color-primary)">
|
||||
<p x-show="passwordForm.confirm_password && passwordForm.new_password !== passwordForm.confirm_password"
|
||||
class="mt-1 text-xs text-red-500">
|
||||
Passwords do not match
|
||||
{{ _('auth.passwords_do_not_match') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style="background-color: var(--color-primary)">
|
||||
<span x-show="changingPassword" class="-ml-1 mr-2 h-4 w-4" x-html="$icon('spinner', 'h-4 w-4 animate-spin')"></span>
|
||||
<span x-text="changingPassword ? 'Changing...' : 'Change Password'"></span>
|
||||
<span x-text="changingPassword ? i18n.changing : i18n.changePassword"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -260,22 +260,22 @@
|
||||
|
||||
<!-- Account Info (read-only) -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Account Information</h3>
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">{{ _('customers.storefront.pages.profile.account_info') }}</h3>
|
||||
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Customer Number</dt>
|
||||
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.customer_number') }}</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.customer_number || '-'"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Member Since</dt>
|
||||
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.member_since') }}</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatDate(profile?.created_at)"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Total Orders</dt>
|
||||
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.total_orders') }}</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.total_orders || 0"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Total Spent</dt>
|
||||
<dt class="text-gray-500 dark:text-gray-400">{{ _('customers.total_spent') }}</dt>
|
||||
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatPrice(profile?.total_spent)"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
@@ -286,9 +286,26 @@
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
window.__shopProfileI18n = {
|
||||
saving: {{ _('customers.storefront.pages.profile.saving')|tojson }},
|
||||
saveChanges: {{ _('customers.storefront.pages.profile.save_changes')|tojson }},
|
||||
savePreferences: {{ _('customers.storefront.pages.profile.save_preferences')|tojson }},
|
||||
changing: {{ _('customers.storefront.pages.profile.changing')|tojson }},
|
||||
changePassword: {{ _('auth.change_password')|tojson }},
|
||||
profileUpdated: {{ _('customers.storefront.pages.profile.profile_updated')|tojson }},
|
||||
preferencesUpdated: {{ _('customers.storefront.pages.profile.preferences_updated')|tojson }},
|
||||
passwordChanged: {{ _('customers.storefront.pages.profile.password_changed')|tojson }},
|
||||
failedToLoad: {{ _('customers.storefront.pages.profile.failed_to_load')|tojson }},
|
||||
failedToSaveProfile: {{ _('customers.storefront.pages.profile.failed_to_save_profile')|tojson }},
|
||||
failedToSavePreferences: {{ _('customers.storefront.pages.profile.failed_to_save_preferences')|tojson }},
|
||||
failedToChangePassword: {{ _('customers.storefront.pages.profile.failed_to_change_password')|tojson }},
|
||||
passwordsDoNotMatch: {{ _('auth.passwords_do_not_match')|tojson }},
|
||||
};
|
||||
function shopProfilePage() {
|
||||
const i18n = window.__shopProfileI18n || {};
|
||||
return {
|
||||
...storefrontLayoutData(),
|
||||
i18n,
|
||||
|
||||
// State
|
||||
profile: null,
|
||||
@@ -347,7 +364,7 @@ function shopProfilePage() {
|
||||
window.location.href = '{{ base_url }}account/login?next=' + encodeURIComponent(window.location.pathname);
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to load profile');
|
||||
throw new Error(i18n.failedToLoad);
|
||||
}
|
||||
|
||||
this.profile = await response.json();
|
||||
@@ -366,7 +383,7 @@ function shopProfilePage() {
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading profile:', err);
|
||||
this.error = err.message || 'Failed to load profile';
|
||||
this.error = err.message || i18n.failedToLoad;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -395,11 +412,11 @@ function shopProfilePage() {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to save profile');
|
||||
throw new Error(error.detail || i18n.failedToSaveProfile);
|
||||
}
|
||||
|
||||
this.profile = await response.json();
|
||||
this.successMessage = 'Profile updated successfully';
|
||||
this.successMessage = i18n.profileUpdated;
|
||||
|
||||
// Update localStorage user data
|
||||
const userStr = localStorage.getItem('customer_user');
|
||||
@@ -415,7 +432,7 @@ function shopProfilePage() {
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error saving profile:', err);
|
||||
this.error = err.message || 'Failed to save profile';
|
||||
this.error = err.message || i18n.failedToSaveProfile;
|
||||
} finally {
|
||||
this.savingProfile = false;
|
||||
}
|
||||
@@ -444,16 +461,16 @@ function shopProfilePage() {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to save preferences');
|
||||
throw new Error(error.detail || i18n.failedToSavePreferences);
|
||||
}
|
||||
|
||||
this.profile = await response.json();
|
||||
this.successMessage = 'Preferences updated successfully';
|
||||
this.successMessage = i18n.preferencesUpdated;
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error saving preferences:', err);
|
||||
this.error = err.message || 'Failed to save preferences';
|
||||
this.error = err.message || i18n.failedToSavePreferences;
|
||||
} finally {
|
||||
this.savingPreferences = false;
|
||||
}
|
||||
@@ -461,7 +478,7 @@ function shopProfilePage() {
|
||||
|
||||
async changePassword() {
|
||||
if (this.passwordForm.new_password !== this.passwordForm.confirm_password) {
|
||||
this.passwordError = 'Passwords do not match';
|
||||
this.passwordError = i18n.passwordsDoNotMatch;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -487,7 +504,7 @@ function shopProfilePage() {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to change password');
|
||||
throw new Error(error.detail || i18n.failedToChangePassword);
|
||||
}
|
||||
|
||||
// Clear password form
|
||||
@@ -497,12 +514,12 @@ function shopProfilePage() {
|
||||
confirm_password: ''
|
||||
};
|
||||
|
||||
this.successMessage = 'Password changed successfully';
|
||||
this.successMessage = i18n.passwordChanged;
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error changing password:', err);
|
||||
this.passwordError = err.message || 'Failed to change password';
|
||||
this.passwordError = err.message || i18n.failedToChangePassword;
|
||||
} finally {
|
||||
this.changingPassword = false;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{# app/templates/storefront/account/reset-password.html #}
|
||||
{# standalone #}
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'dark': dark }" x-data="resetPassword()" lang="en">
|
||||
<html :class="{ 'dark': dark }" x-data="resetPassword()" lang="{{ current_language|default('fr') }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Reset Password - {{ store.name }}</title>
|
||||
<title>{{ _("auth.reset_password") }} - {{ store.name }}</title>
|
||||
<!-- Fonts: Local fallback + Google Fonts -->
|
||||
<link href="{{ static_v(request, 'static', path='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,7 @@
|
||||
<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">Create new password</p>
|
||||
<p class="text-white opacity-90">{{ _("auth.reset_password_subtitle") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,21 +68,22 @@
|
||||
<template x-if="tokenInvalid">
|
||||
<div class="text-center">
|
||||
<div class="flex items-center justify-center w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900">
|
||||
<span class="w-8 h-8 text-red-600 dark:text-red-400" x-html="$icon('x-mark', 'w-8 h-8')"></span>
|
||||
<svg class="w-8 h-8 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Invalid or Expired Link
|
||||
{{ _("auth.invalid_or_expired_link") }}
|
||||
</h1>
|
||||
|
||||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
This password reset link is invalid or has expired.
|
||||
Please request a new password reset link.
|
||||
{{ _("auth.invalid_or_expired_link_desc") }}
|
||||
</p>
|
||||
|
||||
<a href="{{ base_url }}account/forgot-password"
|
||||
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
|
||||
Request New Link
|
||||
{{ _("auth.request_new_link") }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -91,11 +92,11 @@
|
||||
<template x-if="!tokenInvalid && !resetComplete">
|
||||
<div>
|
||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Reset Your Password
|
||||
{{ _("auth.reset_your_password") }}
|
||||
</h1>
|
||||
|
||||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
Enter your new password below. Password must be at least 8 characters.
|
||||
{{ _("auth.reset_password_form_desc") }}
|
||||
</p>
|
||||
|
||||
<!-- Error Message -->
|
||||
@@ -107,14 +108,14 @@
|
||||
<!-- Reset Password Form -->
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">New Password</span>
|
||||
<span class="text-gray-700 dark:text-gray-400">{{ _("auth.new_password") }}</span>
|
||||
<input x-model="password"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
type="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 new password"
|
||||
placeholder="{{ _('auth.new_password_placeholder') }}"
|
||||
autocomplete="new-password"
|
||||
required />
|
||||
<span x-show="errors.password" x-text="errors.password"
|
||||
@@ -122,14 +123,14 @@
|
||||
</label>
|
||||
|
||||
<label class="block mt-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Confirm Password</span>
|
||||
<span class="text-gray-700 dark:text-gray-400">{{ _("auth.confirm_password") }}</span>
|
||||
<input x-model="confirmPassword"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
type="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.confirmPassword }"
|
||||
placeholder="Confirm new password"
|
||||
placeholder="{{ _('auth.confirm_password_placeholder') }}"
|
||||
autocomplete="new-password"
|
||||
required />
|
||||
<span x-show="errors.confirmPassword" x-text="errors.confirmPassword"
|
||||
@@ -138,10 +139,13 @@
|
||||
|
||||
<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">Reset Password</span>
|
||||
<span x-show="!loading">{{ _("auth.reset_password_btn") }}</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>
|
||||
Resetting...
|
||||
<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.resetting") }}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
@@ -152,21 +156,22 @@
|
||||
<template x-if="resetComplete">
|
||||
<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">
|
||||
Password Reset Complete
|
||||
{{ _("auth.password_reset_complete") }}
|
||||
</h1>
|
||||
|
||||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
Your password has been successfully reset.
|
||||
You can now sign in with your new password.
|
||||
{{ _("auth.password_reset_success_desc") }}
|
||||
</p>
|
||||
|
||||
<a href="{{ base_url }}account/login"
|
||||
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
|
||||
Sign In
|
||||
{{ _("auth.sign_in") }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -174,19 +179,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
|
||||
← {{ _("auth.back_to_home") }}
|
||||
</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>
|
||||
@@ -196,9 +216,37 @@
|
||||
<!-- Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
{# Translated client-side strings — kept in sync with auth.* keys above #}
|
||||
<script>
|
||||
window.__resetPasswordI18n = {
|
||||
passwordRequired: {{ _('auth.password_required')|tojson }},
|
||||
passwordTooShort: {{ _('auth.password_too_short')|tojson }},
|
||||
pleaseConfirmPassword: {{ _('auth.please_confirm_password')|tojson }},
|
||||
passwordsDoNotMatch: {{ _('auth.passwords_do_not_match')|tojson }},
|
||||
resetPasswordFailed: {{ _('auth.reset_password_failed')|tojson }},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Reset 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 resetPassword() {
|
||||
const i18n = window.__resetPasswordI18n || {};
|
||||
return {
|
||||
// Data
|
||||
token: '',
|
||||
@@ -251,22 +299,22 @@
|
||||
|
||||
// Validation
|
||||
if (!this.password) {
|
||||
this.errors.password = 'Password is required';
|
||||
this.errors.password = i18n.passwordRequired;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password.length < 8) {
|
||||
this.errors.password = 'Password must be at least 8 characters';
|
||||
this.errors.password = i18n.passwordTooShort;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.confirmPassword) {
|
||||
this.errors.confirmPassword = 'Please confirm your password';
|
||||
this.errors.confirmPassword = i18n.pleaseConfirmPassword;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password !== this.confirmPassword) {
|
||||
this.errors.confirmPassword = 'Passwords do not match';
|
||||
this.errors.confirmPassword = i18n.passwordsDoNotMatch;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -294,7 +342,7 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(data.detail || 'Failed to reset password');
|
||||
throw new Error(data.detail || i18n.resetPasswordFailed);
|
||||
}
|
||||
|
||||
// Success
|
||||
@@ -302,7 +350,7 @@
|
||||
|
||||
} catch (error) {
|
||||
console.error('Reset password error:', error);
|
||||
this.showAlert(error.message || 'Failed to reset password. Please try again.');
|
||||
this.showAlert(error.message || i18n.resetPasswordFailed);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user