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:
@@ -59,6 +59,95 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"addresses": "Adressen",
|
"addresses": "Adressen",
|
||||||
"settings": "Einstellungen"
|
"settings": "Einstellungen"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Mein Konto",
|
||||||
|
"welcome_back": "Willkommen zurück, {name}!",
|
||||||
|
"profile_card_title": "Profil",
|
||||||
|
"profile_card_subtitle": "Informationen bearbeiten",
|
||||||
|
"addresses_card_title": "Adressen",
|
||||||
|
"addresses_card_subtitle": "Adressen verwalten",
|
||||||
|
"messages_card_title": "Nachrichten",
|
||||||
|
"messages_card_subtitle": "Support kontaktieren",
|
||||||
|
"unread_messages_singular": "{count} ungelesene Nachricht",
|
||||||
|
"unread_messages_plural": "{count} ungelesene Nachrichten",
|
||||||
|
"summary_title": "Kontoübersicht",
|
||||||
|
"customer_since": "Kunde seit",
|
||||||
|
"logout": "Abmelden",
|
||||||
|
"logout_confirm_title": "Abmeldung bestätigen",
|
||||||
|
"logout_confirm_message": "Sind Sie sicher, dass Sie sich abmelden möchten? Sie müssen sich erneut anmelden, um auf Ihr Konto zuzugreifen.",
|
||||||
|
"logout_success": "Abmeldung erfolgreich",
|
||||||
|
"logout_failed": "Abmeldung fehlgeschlagen"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"breadcrumb_account": "Mein Konto",
|
||||||
|
"breadcrumb_profile": "Profil",
|
||||||
|
"title": "Mein Profil",
|
||||||
|
"subtitle": "Verwalten Sie Ihre Kontoinformationen und Einstellungen",
|
||||||
|
"info_section_title": "Profilinformationen",
|
||||||
|
"info_section_subtitle": "Persönliche Daten aktualisieren",
|
||||||
|
"email_label": "E-Mail-Adresse",
|
||||||
|
"save_changes": "Änderungen speichern",
|
||||||
|
"saving": "Speichern...",
|
||||||
|
"prefs_section_title": "Einstellungen",
|
||||||
|
"prefs_section_subtitle": "Verwalten Sie Ihre Kontoeinstellungen",
|
||||||
|
"preferred_language": "Bevorzugte Sprache",
|
||||||
|
"use_shop_default": "Shop-Standard verwenden",
|
||||||
|
"marketing_communications": "Marketing-Kommunikation",
|
||||||
|
"marketing_desc": "E-Mails über neue Produkte, Angebote und Aktionen erhalten",
|
||||||
|
"save_preferences": "Einstellungen speichern",
|
||||||
|
"change_password_subtitle": "Aktualisieren Sie Ihr Kontopasswort",
|
||||||
|
"changing": "Wird geändert...",
|
||||||
|
"account_info": "Kontoinformationen",
|
||||||
|
"member_since": "Mitglied seit",
|
||||||
|
"profile_updated": "Profil erfolgreich aktualisiert",
|
||||||
|
"preferences_updated": "Einstellungen erfolgreich aktualisiert",
|
||||||
|
"password_changed": "Passwort erfolgreich geändert",
|
||||||
|
"failed_to_load": "Profil konnte nicht geladen werden",
|
||||||
|
"failed_to_save_profile": "Profil konnte nicht gespeichert werden",
|
||||||
|
"failed_to_save_preferences": "Einstellungen konnten nicht gespeichert werden",
|
||||||
|
"failed_to_change_password": "Passwort konnte nicht geändert werden"
|
||||||
|
},
|
||||||
|
"addresses": {
|
||||||
|
"title": "Meine Adressen",
|
||||||
|
"subtitle": "Verwalten Sie Ihre Liefer- und Rechnungsadressen",
|
||||||
|
"add_address": "Adresse hinzufügen",
|
||||||
|
"empty_state_title": "Noch keine Adressen",
|
||||||
|
"empty_state_subtitle": "Fügen Sie Ihre erste Adresse hinzu, um den Bezahlvorgang zu beschleunigen.",
|
||||||
|
"add_first_address": "Erste Adresse hinzufügen",
|
||||||
|
"default_shipping": "Standard-Lieferadresse",
|
||||||
|
"default_billing": "Standard-Rechnungsadresse",
|
||||||
|
"shipping": "Lieferung",
|
||||||
|
"billing": "Rechnung",
|
||||||
|
"set_default": "Als Standard festlegen",
|
||||||
|
"edit_address": "Adresse bearbeiten",
|
||||||
|
"add_new_address": "Neue Adresse",
|
||||||
|
"address_type": "Adresstyp",
|
||||||
|
"shipping_address": "Lieferadresse",
|
||||||
|
"billing_address": "Rechnungsadresse",
|
||||||
|
"company_optional": "Unternehmen (optional)",
|
||||||
|
"address_line_1": "Adresse",
|
||||||
|
"address_line_2_optional": "Adresszusatz (optional)",
|
||||||
|
"postal_code": "Postleitzahl",
|
||||||
|
"city": "Stadt",
|
||||||
|
"country": "Land",
|
||||||
|
"set_as_default_shipping": "Als Standard-Lieferadresse festlegen",
|
||||||
|
"set_as_default_billing": "Als Standard-Rechnungsadresse festlegen",
|
||||||
|
"save_changes": "Änderungen speichern",
|
||||||
|
"saving": "Speichern...",
|
||||||
|
"delete_address": "Adresse löschen",
|
||||||
|
"delete_confirm": "Sind Sie sicher, dass Sie diese Adresse löschen möchten? Dieser Vorgang ist nicht rückgängig zu machen.",
|
||||||
|
"deleting": "Wird gelöscht...",
|
||||||
|
"address_updated": "Adresse aktualisiert",
|
||||||
|
"address_added": "Adresse hinzugefügt",
|
||||||
|
"address_deleted": "Adresse gelöscht",
|
||||||
|
"default_updated": "Standardadresse aktualisiert",
|
||||||
|
"failed_to_load": "Adressen konnten nicht geladen werden. Bitte erneut versuchen.",
|
||||||
|
"failed_to_save": "Adresse konnte nicht gespeichert werden. Bitte erneut versuchen.",
|
||||||
|
"failed_to_delete": "Adresse konnte nicht gelöscht werden",
|
||||||
|
"failed_to_set_default": "Standardadresse konnte nicht festgelegt werden"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,95 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"addresses": "Addresses",
|
"addresses": "Addresses",
|
||||||
"settings": "Settings"
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"dashboard": {
|
||||||
|
"title": "My Account",
|
||||||
|
"welcome_back": "Welcome back, {name}!",
|
||||||
|
"profile_card_title": "Profile",
|
||||||
|
"profile_card_subtitle": "Edit your information",
|
||||||
|
"addresses_card_title": "Addresses",
|
||||||
|
"addresses_card_subtitle": "Manage addresses",
|
||||||
|
"messages_card_title": "Messages",
|
||||||
|
"messages_card_subtitle": "Contact support",
|
||||||
|
"unread_messages_singular": "{count} unread message",
|
||||||
|
"unread_messages_plural": "{count} unread messages",
|
||||||
|
"summary_title": "Account Summary",
|
||||||
|
"customer_since": "Customer Since",
|
||||||
|
"logout": "Logout",
|
||||||
|
"logout_confirm_title": "Logout Confirmation",
|
||||||
|
"logout_confirm_message": "Are you sure you want to logout? You'll need to sign in again to access your account.",
|
||||||
|
"logout_success": "Logged out successfully",
|
||||||
|
"logout_failed": "Logout failed"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"breadcrumb_account": "My Account",
|
||||||
|
"breadcrumb_profile": "Profile",
|
||||||
|
"title": "My Profile",
|
||||||
|
"subtitle": "Manage your account information and preferences",
|
||||||
|
"info_section_title": "Profile Information",
|
||||||
|
"info_section_subtitle": "Update your personal details",
|
||||||
|
"email_label": "Email Address",
|
||||||
|
"save_changes": "Save Changes",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"prefs_section_title": "Preferences",
|
||||||
|
"prefs_section_subtitle": "Manage your account preferences",
|
||||||
|
"preferred_language": "Preferred Language",
|
||||||
|
"use_shop_default": "Use shop default",
|
||||||
|
"marketing_communications": "Marketing Communications",
|
||||||
|
"marketing_desc": "Receive emails about new products, offers, and promotions",
|
||||||
|
"save_preferences": "Save Preferences",
|
||||||
|
"change_password_subtitle": "Update your account password",
|
||||||
|
"changing": "Changing...",
|
||||||
|
"account_info": "Account Information",
|
||||||
|
"member_since": "Member Since",
|
||||||
|
"profile_updated": "Profile updated successfully",
|
||||||
|
"preferences_updated": "Preferences updated successfully",
|
||||||
|
"password_changed": "Password changed successfully",
|
||||||
|
"failed_to_load": "Failed to load profile",
|
||||||
|
"failed_to_save_profile": "Failed to save profile",
|
||||||
|
"failed_to_save_preferences": "Failed to save preferences",
|
||||||
|
"failed_to_change_password": "Failed to change password"
|
||||||
|
},
|
||||||
|
"addresses": {
|
||||||
|
"title": "My Addresses",
|
||||||
|
"subtitle": "Manage your shipping and billing addresses",
|
||||||
|
"add_address": "Add Address",
|
||||||
|
"empty_state_title": "No addresses yet",
|
||||||
|
"empty_state_subtitle": "Add your first address to speed up checkout.",
|
||||||
|
"add_first_address": "Add Your First Address",
|
||||||
|
"default_shipping": "Default Shipping",
|
||||||
|
"default_billing": "Default Billing",
|
||||||
|
"shipping": "Shipping",
|
||||||
|
"billing": "Billing",
|
||||||
|
"set_default": "Set as Default",
|
||||||
|
"edit_address": "Edit Address",
|
||||||
|
"add_new_address": "Add New Address",
|
||||||
|
"address_type": "Address Type",
|
||||||
|
"shipping_address": "Shipping Address",
|
||||||
|
"billing_address": "Billing Address",
|
||||||
|
"company_optional": "Company (optional)",
|
||||||
|
"address_line_1": "Address",
|
||||||
|
"address_line_2_optional": "Address Line 2 (optional)",
|
||||||
|
"postal_code": "Postal Code",
|
||||||
|
"city": "City",
|
||||||
|
"country": "Country",
|
||||||
|
"set_as_default_shipping": "Set as default shipping address",
|
||||||
|
"set_as_default_billing": "Set as default billing address",
|
||||||
|
"save_changes": "Save Changes",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"delete_address": "Delete Address",
|
||||||
|
"delete_confirm": "Are you sure you want to delete this address? This action cannot be undone.",
|
||||||
|
"deleting": "Deleting...",
|
||||||
|
"address_updated": "Address updated",
|
||||||
|
"address_added": "Address added",
|
||||||
|
"address_deleted": "Address deleted",
|
||||||
|
"default_updated": "Default address updated",
|
||||||
|
"failed_to_load": "Failed to load addresses. Please try again.",
|
||||||
|
"failed_to_save": "Failed to save address. Please try again.",
|
||||||
|
"failed_to_delete": "Failed to delete address",
|
||||||
|
"failed_to_set_default": "Failed to set default address"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,95 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"addresses": "Adresses",
|
"addresses": "Adresses",
|
||||||
"settings": "Paramètres"
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Mon compte",
|
||||||
|
"welcome_back": "Bon retour, {name} !",
|
||||||
|
"profile_card_title": "Profil",
|
||||||
|
"profile_card_subtitle": "Modifier vos informations",
|
||||||
|
"addresses_card_title": "Adresses",
|
||||||
|
"addresses_card_subtitle": "Gérer vos adresses",
|
||||||
|
"messages_card_title": "Messages",
|
||||||
|
"messages_card_subtitle": "Contacter le support",
|
||||||
|
"unread_messages_singular": "{count} message non lu",
|
||||||
|
"unread_messages_plural": "{count} messages non lus",
|
||||||
|
"summary_title": "Résumé du compte",
|
||||||
|
"customer_since": "Client depuis",
|
||||||
|
"logout": "Se déconnecter",
|
||||||
|
"logout_confirm_title": "Confirmer la déconnexion",
|
||||||
|
"logout_confirm_message": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez vous reconnecter pour accéder à votre compte.",
|
||||||
|
"logout_success": "Déconnexion réussie",
|
||||||
|
"logout_failed": "Échec de la déconnexion"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"breadcrumb_account": "Mon compte",
|
||||||
|
"breadcrumb_profile": "Profil",
|
||||||
|
"title": "Mon profil",
|
||||||
|
"subtitle": "Gérez les informations et préférences de votre compte",
|
||||||
|
"info_section_title": "Informations du profil",
|
||||||
|
"info_section_subtitle": "Mettez à jour vos informations personnelles",
|
||||||
|
"email_label": "Adresse e-mail",
|
||||||
|
"save_changes": "Enregistrer",
|
||||||
|
"saving": "Enregistrement...",
|
||||||
|
"prefs_section_title": "Préférences",
|
||||||
|
"prefs_section_subtitle": "Gérez les préférences de votre compte",
|
||||||
|
"preferred_language": "Langue préférée",
|
||||||
|
"use_shop_default": "Utiliser la langue de la boutique",
|
||||||
|
"marketing_communications": "Communications marketing",
|
||||||
|
"marketing_desc": "Recevoir des e-mails sur les nouveaux produits, offres et promotions",
|
||||||
|
"save_preferences": "Enregistrer les préférences",
|
||||||
|
"change_password_subtitle": "Mettez à jour le mot de passe de votre compte",
|
||||||
|
"changing": "Changement...",
|
||||||
|
"account_info": "Informations du compte",
|
||||||
|
"member_since": "Membre depuis",
|
||||||
|
"profile_updated": "Profil mis à jour avec succès",
|
||||||
|
"preferences_updated": "Préférences mises à jour avec succès",
|
||||||
|
"password_changed": "Mot de passe modifié avec succès",
|
||||||
|
"failed_to_load": "Échec du chargement du profil",
|
||||||
|
"failed_to_save_profile": "Échec de l'enregistrement du profil",
|
||||||
|
"failed_to_save_preferences": "Échec de l'enregistrement des préférences",
|
||||||
|
"failed_to_change_password": "Échec du changement de mot de passe"
|
||||||
|
},
|
||||||
|
"addresses": {
|
||||||
|
"title": "Mes adresses",
|
||||||
|
"subtitle": "Gérez vos adresses de livraison et de facturation",
|
||||||
|
"add_address": "Ajouter une adresse",
|
||||||
|
"empty_state_title": "Aucune adresse",
|
||||||
|
"empty_state_subtitle": "Ajoutez votre première adresse pour accélérer le paiement.",
|
||||||
|
"add_first_address": "Ajouter votre première adresse",
|
||||||
|
"default_shipping": "Livraison par défaut",
|
||||||
|
"default_billing": "Facturation par défaut",
|
||||||
|
"shipping": "Livraison",
|
||||||
|
"billing": "Facturation",
|
||||||
|
"set_default": "Définir par défaut",
|
||||||
|
"edit_address": "Modifier l'adresse",
|
||||||
|
"add_new_address": "Nouvelle adresse",
|
||||||
|
"address_type": "Type d'adresse",
|
||||||
|
"shipping_address": "Adresse de livraison",
|
||||||
|
"billing_address": "Adresse de facturation",
|
||||||
|
"company_optional": "Société (facultatif)",
|
||||||
|
"address_line_1": "Adresse",
|
||||||
|
"address_line_2_optional": "Complément d'adresse (facultatif)",
|
||||||
|
"postal_code": "Code postal",
|
||||||
|
"city": "Ville",
|
||||||
|
"country": "Pays",
|
||||||
|
"set_as_default_shipping": "Définir comme adresse de livraison par défaut",
|
||||||
|
"set_as_default_billing": "Définir comme adresse de facturation par défaut",
|
||||||
|
"save_changes": "Enregistrer",
|
||||||
|
"saving": "Enregistrement...",
|
||||||
|
"delete_address": "Supprimer l'adresse",
|
||||||
|
"delete_confirm": "Êtes-vous sûr de vouloir supprimer cette adresse ? Cette action est irréversible.",
|
||||||
|
"deleting": "Suppression...",
|
||||||
|
"address_updated": "Adresse mise à jour",
|
||||||
|
"address_added": "Adresse ajoutée",
|
||||||
|
"address_deleted": "Adresse supprimée",
|
||||||
|
"default_updated": "Adresse par défaut mise à jour",
|
||||||
|
"failed_to_load": "Échec du chargement des adresses. Veuillez réessayer.",
|
||||||
|
"failed_to_save": "Échec de l'enregistrement de l'adresse. Veuillez réessayer.",
|
||||||
|
"failed_to_delete": "Échec de la suppression de l'adresse",
|
||||||
|
"failed_to_set_default": "Échec de la définition de l'adresse par défaut"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,95 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"addresses": "Adressen",
|
"addresses": "Adressen",
|
||||||
"settings": "Astellungen"
|
"settings": "Astellungen"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Mäi Kont",
|
||||||
|
"welcome_back": "Wëllkomm zréck, {name} !",
|
||||||
|
"profile_card_title": "Profil",
|
||||||
|
"profile_card_subtitle": "Informatiounen änneren",
|
||||||
|
"addresses_card_title": "Adressen",
|
||||||
|
"addresses_card_subtitle": "Adressen verwalten",
|
||||||
|
"messages_card_title": "Noriichten",
|
||||||
|
"messages_card_subtitle": "Support kontaktéieren",
|
||||||
|
"unread_messages_singular": "{count} ongeliesen Noriicht",
|
||||||
|
"unread_messages_plural": "{count} ongeliesen Noriichten",
|
||||||
|
"summary_title": "Kontoiwwersiicht",
|
||||||
|
"customer_since": "Client zënter",
|
||||||
|
"logout": "Ofmellen",
|
||||||
|
"logout_confirm_title": "Ofmellen bestätegen",
|
||||||
|
"logout_confirm_message": "Sidd Dir sécher datt Dir Iech ofmellen wëllt? Dir musst Iech erëm aloggen fir op Äre Kont ze zougräifen.",
|
||||||
|
"logout_success": "Erfollegräich ofgemellt",
|
||||||
|
"logout_failed": "Ofmellen feelgeschloen"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"breadcrumb_account": "Mäi Kont",
|
||||||
|
"breadcrumb_profile": "Profil",
|
||||||
|
"title": "Mäi Profil",
|
||||||
|
"subtitle": "Verwalt Är Kontodaten an Astellungen",
|
||||||
|
"info_section_title": "Profilinformatiounen",
|
||||||
|
"info_section_subtitle": "Är perséinlech Donnéeën updaten",
|
||||||
|
"email_label": "E-Mail-Adress",
|
||||||
|
"save_changes": "Änneren späicheren",
|
||||||
|
"saving": "Späicheren...",
|
||||||
|
"prefs_section_title": "Astellungen",
|
||||||
|
"prefs_section_subtitle": "Verwalt Är Kontosastellungen",
|
||||||
|
"preferred_language": "Bevorzucht Sprooch",
|
||||||
|
"use_shop_default": "Buttek-Standard benotzen",
|
||||||
|
"marketing_communications": "Marketing-Kommunikatioun",
|
||||||
|
"marketing_desc": "E-Maile mat neie Produkter, Offeren a Promotioune kréien",
|
||||||
|
"save_preferences": "Astellunge späicheren",
|
||||||
|
"change_password_subtitle": "Aktualiséiert Äert Kontopasswuert",
|
||||||
|
"changing": "Änneren...",
|
||||||
|
"account_info": "Kontoinformatiounen",
|
||||||
|
"member_since": "Member zënter",
|
||||||
|
"profile_updated": "Profil erfollegräich aktualiséiert",
|
||||||
|
"preferences_updated": "Astellungen erfollegräich aktualiséiert",
|
||||||
|
"password_changed": "Passwuert erfollegräich geännert",
|
||||||
|
"failed_to_load": "Profil konnt net geluede ginn",
|
||||||
|
"failed_to_save_profile": "Profil konnt net späichert ginn",
|
||||||
|
"failed_to_save_preferences": "Astellunge konnten net späichert ginn",
|
||||||
|
"failed_to_change_password": "Passwuert konnt net geännert ginn"
|
||||||
|
},
|
||||||
|
"addresses": {
|
||||||
|
"title": "Meng Adressen",
|
||||||
|
"subtitle": "Verwalt Är Liwwer- a Rechnungsadressen",
|
||||||
|
"add_address": "Adress derbäisetzen",
|
||||||
|
"empty_state_title": "Nach keng Adressen",
|
||||||
|
"empty_state_subtitle": "Setzt Är éischt Adress derbäi fir den Bezuelvirgang ze beschleunegen.",
|
||||||
|
"add_first_address": "Éischt Adress derbäisetzen",
|
||||||
|
"default_shipping": "Standard-Liwweradress",
|
||||||
|
"default_billing": "Standard-Rechnungsadress",
|
||||||
|
"shipping": "Liwwerung",
|
||||||
|
"billing": "Rechnung",
|
||||||
|
"set_default": "Als Standard festleeën",
|
||||||
|
"edit_address": "Adress änneren",
|
||||||
|
"add_new_address": "Nei Adress",
|
||||||
|
"address_type": "Adresstyp",
|
||||||
|
"shipping_address": "Liwweradress",
|
||||||
|
"billing_address": "Rechnungsadress",
|
||||||
|
"company_optional": "Entreprise (fakultativ)",
|
||||||
|
"address_line_1": "Adress",
|
||||||
|
"address_line_2_optional": "Adresszousatz (fakultativ)",
|
||||||
|
"postal_code": "Postleitzuel",
|
||||||
|
"city": "Stad",
|
||||||
|
"country": "Land",
|
||||||
|
"set_as_default_shipping": "Als Standard-Liwweradress festleeën",
|
||||||
|
"set_as_default_billing": "Als Standard-Rechnungsadress festleeën",
|
||||||
|
"save_changes": "Änneren späicheren",
|
||||||
|
"saving": "Späicheren...",
|
||||||
|
"delete_address": "Adress läschen",
|
||||||
|
"delete_confirm": "Sidd Dir sécher datt Dir dës Adress läsche wëllt? Dës Aktioun kann net réckgängeg gemaach ginn.",
|
||||||
|
"deleting": "Läschen...",
|
||||||
|
"address_updated": "Adress aktualiséiert",
|
||||||
|
"address_added": "Adress derbäigesat",
|
||||||
|
"address_deleted": "Adress geläscht",
|
||||||
|
"default_updated": "Standardadress aktualiséiert",
|
||||||
|
"failed_to_load": "Adressen konnten net geluede ginn. Probéiert nach eng Kéier.",
|
||||||
|
"failed_to_save": "Adress konnt net späichert ginn. Probéiert nach eng Kéier.",
|
||||||
|
"failed_to_delete": "Adress konnt net geläscht ginn",
|
||||||
|
"failed_to_set_default": "Standardadress konnt net festgeluecht ginn"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,11 +270,12 @@ def forgot_password(
|
|||||||
reset_link = f"{scheme}://{host}/account/reset-password?token={plaintext_token}"
|
reset_link = f"{scheme}://{host}/account/reset-password?token={plaintext_token}"
|
||||||
|
|
||||||
email_service = EmailService(db)
|
email_service = EmailService(db)
|
||||||
|
request_language = getattr(request.state, "language", None)
|
||||||
email_service.send_template(
|
email_service.send_template(
|
||||||
template_code="password_reset",
|
template_code="password_reset",
|
||||||
to_email=customer.email,
|
to_email=customer.email,
|
||||||
to_name=customer.full_name,
|
to_name=customer.full_name,
|
||||||
language=customer.preferred_language or "en",
|
language=request_language or customer.preferred_language or "en",
|
||||||
variables={
|
variables={
|
||||||
"customer_name": customer.first_name or customer.full_name,
|
"customer_name": customer.first_name or customer.full_name,
|
||||||
"reset_link": reset_link,
|
"reset_link": reset_link,
|
||||||
|
|||||||
@@ -568,6 +568,7 @@ class CustomerService:
|
|||||||
last_name: str = "",
|
last_name: str = "",
|
||||||
phone: str | None = None,
|
phone: str | None = None,
|
||||||
birth_date: date | None = None,
|
birth_date: date | None = None,
|
||||||
|
preferred_language: str | None = None,
|
||||||
) -> Customer:
|
) -> Customer:
|
||||||
"""
|
"""
|
||||||
Create a customer for loyalty/external enrollment.
|
Create a customer for loyalty/external enrollment.
|
||||||
@@ -606,6 +607,7 @@ class CustomerService:
|
|||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
phone=phone,
|
phone=phone,
|
||||||
birth_date=birth_date,
|
birth_date=birth_date,
|
||||||
|
preferred_language=preferred_language,
|
||||||
hashed_password=unusable_hash,
|
hashed_password=unusable_hash,
|
||||||
customer_number=cust_number,
|
customer_number=cust_number,
|
||||||
store_id=store_id,
|
store_id=store_id,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{# app/templates/storefront/account/addresses.html #}
|
{# app/templates/storefront/account/addresses.html #}
|
||||||
{% extends "storefront/base.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 %}
|
{% block alpine_data %}addressesPage(){% endblock %}
|
||||||
|
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Addresses</h1>
|
<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">Manage your shipping and billing addresses</p>
|
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.addresses.subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="openAddModal()"
|
<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"
|
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)">
|
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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@
|
|||||||
<div x-show="!loading && !error && addresses.length === 0"
|
<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">
|
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>
|
<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>
|
<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">Add your first address to speed up checkout.</p>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.addresses.empty_state_subtitle') }}</p>
|
||||||
<button @click="openAddModal()"
|
<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"
|
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)">
|
style="background-color: var(--color-primary)">
|
||||||
Add Your First Address
|
{{ _('customers.storefront.pages.addresses.add_first_address') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,14 +56,14 @@
|
|||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
<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'">
|
: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>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Address Type Badge (non-default) -->
|
<!-- Address Type Badge (non-default) -->
|
||||||
<div x-show="!address.is_default" class="absolute top-4 right-4">
|
<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"
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Address Content -->
|
<!-- Address Content -->
|
||||||
@@ -81,16 +81,16 @@
|
|||||||
<button @click="openEditModal(address)"
|
<button @click="openEditModal(address)"
|
||||||
class="text-sm font-medium text-primary hover:text-primary-dark"
|
class="text-sm font-medium text-primary hover:text-primary-dark"
|
||||||
style="color: var(--color-primary)">
|
style="color: var(--color-primary)">
|
||||||
Edit
|
{{ _('common.edit') }}
|
||||||
</button>
|
</button>
|
||||||
<button x-show="!address.is_default"
|
<button x-show="!address.is_default"
|
||||||
@click="setAsDefault(address.id)"
|
@click="setAsDefault(address.id)"
|
||||||
class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
|
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>
|
||||||
<button @click="openDeleteModal(address.id)"
|
<button @click="openDeleteModal(address.id)"
|
||||||
class="text-sm font-medium text-red-600 hover:text-red-700">
|
class="text-sm font-medium text-red-600 hover:text-red-700">
|
||||||
Delete
|
{{ _('common.delete') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,28 +140,28 @@
|
|||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-6"
|
<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">
|
<form @submit.prevent="saveAddress()" class="space-y-4">
|
||||||
<!-- Address Type -->
|
<!-- Address Type -->
|
||||||
<div>
|
<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"
|
<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">
|
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="shipping">{{ _('customers.storefront.pages.addresses.shipping_address') }}</option>
|
||||||
<option value="billing">Billing Address</option>
|
<option value="billing">{{ _('customers.storefront.pages.addresses.billing_address') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Name Row -->
|
<!-- Name Row -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<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
|
<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">
|
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>
|
||||||
<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
|
<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">
|
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>
|
||||||
@@ -169,21 +169,21 @@
|
|||||||
|
|
||||||
<!-- Company -->
|
<!-- Company -->
|
||||||
<div>
|
<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"
|
<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">
|
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>
|
||||||
|
|
||||||
<!-- Address Line 1 -->
|
<!-- Address Line 1 -->
|
||||||
<div>
|
<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
|
<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">
|
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>
|
||||||
|
|
||||||
<!-- Address Line 2 -->
|
<!-- Address Line 2 -->
|
||||||
<div>
|
<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"
|
<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">
|
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>
|
||||||
@@ -191,12 +191,12 @@
|
|||||||
<!-- City & Postal Code -->
|
<!-- City & Postal Code -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<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
|
<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">
|
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>
|
||||||
<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
|
<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">
|
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>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
|
|
||||||
<!-- Country -->
|
<!-- Country -->
|
||||||
<div>
|
<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"
|
<select x-model="addressForm.country_iso"
|
||||||
@change="addressForm.country_name = countries.find(c => c.iso === addressForm.country_iso)?.name || ''"
|
@change="addressForm.country_name = countries.find(c => c.iso === addressForm.country_iso)?.name || ''"
|
||||||
required
|
required
|
||||||
@@ -220,9 +220,8 @@
|
|||||||
<input type="checkbox" x-model="addressForm.is_default"
|
<input type="checkbox" x-model="addressForm.is_default"
|
||||||
class="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary"
|
class="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary"
|
||||||
style="color: var(--color-primary)">
|
style="color: var(--color-primary)">
|
||||||
<label class="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
<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
|
x-text="addressForm.address_type === 'shipping' ? i18n.setAsDefaultShipping : i18n.setAsDefaultBilling"></label>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
@@ -234,14 +233,14 @@
|
|||||||
<div class="mt-6 flex justify-end space-x-3">
|
<div class="mt-6 flex justify-end space-x-3">
|
||||||
<button type="button" @click="showAddressModal = false"
|
<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">
|
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>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
:disabled="saving"
|
: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"
|
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)">
|
style="background-color: var(--color-primary)">
|
||||||
<span x-show="!saving" x-text="editingAddress ? 'Save Changes' : 'Add Address'"></span>
|
<span x-show="!saving" x-text="editingAddress ? i18n.saveChanges : i18n.addAddress"></span>
|
||||||
<span x-show="saving">Saving...</span>
|
<span x-show="saving">{{ _('customers.storefront.pages.addresses.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
<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>
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<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">
|
<div class="mt-2">
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,12 +300,12 @@
|
|||||||
<button @click="confirmDelete()"
|
<button @click="confirmDelete()"
|
||||||
:disabled="deleting"
|
: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">
|
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">{{ _('common.delete') }}</span>
|
||||||
<span x-show="deleting">Deleting...</span>
|
<span x-show="deleting">{{ _('customers.storefront.pages.addresses.deleting') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="showDeleteModal = false"
|
<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">
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -316,9 +315,31 @@
|
|||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
<script>
|
<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() {
|
function addressesPage() {
|
||||||
|
const i18n = window.__addressesPageI18n || {};
|
||||||
return {
|
return {
|
||||||
...storefrontLayoutData(),
|
...storefrontLayoutData(),
|
||||||
|
i18n,
|
||||||
|
|
||||||
// State
|
// State
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -403,14 +424,14 @@ function addressesPage() {
|
|||||||
window.location.href = '{{ base_url }}account/login';
|
window.location.href = '{{ base_url }}account/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error('Failed to load addresses');
|
throw new Error(i18n.failedToLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.addresses = data.addresses;
|
this.addresses = data.addresses;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ADDRESSES] Error loading:', err);
|
console.error('[ADDRESSES] Error loading:', err);
|
||||||
this.error = 'Failed to load addresses. Please try again.';
|
this.error = i18n.failedToLoad;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -476,15 +497,15 @@ function addressesPage() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json();
|
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.showAddressModal = false;
|
||||||
this.showToast(this.editingAddress ? 'Address updated' : 'Address added', 'success');
|
this.showToast(this.editingAddress ? i18n.addressUpdated : i18n.addressAdded, 'success');
|
||||||
await this.loadAddresses();
|
await this.loadAddresses();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ADDRESSES] Error saving:', 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 {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
@@ -508,15 +529,15 @@ function addressesPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete address');
|
throw new Error(i18n.failedToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showDeleteModal = false;
|
this.showDeleteModal = false;
|
||||||
this.showToast('Address deleted', 'success');
|
this.showToast(i18n.addressDeleted, 'success');
|
||||||
await this.loadAddresses();
|
await this.loadAddresses();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ADDRESSES] Error deleting:', err);
|
console.error('[ADDRESSES] Error deleting:', err);
|
||||||
this.showToast('Failed to delete address', 'error');
|
this.showToast(i18n.failedToDelete, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
this.deleting = false;
|
this.deleting = false;
|
||||||
}
|
}
|
||||||
@@ -533,14 +554,14 @@ function addressesPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
await this.loadAddresses();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ADDRESSES] Error setting default:', 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" %}
|
{% extends "storefront/base.html" %}
|
||||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
{% 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 %}
|
{% 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">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Account</h1>
|
<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">Welcome back, {{ user.first_name }}!</p>
|
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.welcome_back', name=user.first_name) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dashboard Grid -->
|
<!-- 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>
|
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('user', 'h-8 w-8')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Profile</h3>
|
<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">Edit your information</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.profile_card_subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Addresses</h3>
|
<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">Manage addresses</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.addresses_card_subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -85,26 +85,27 @@
|
|||||||
x-text="unreadCount > 9 ? '9+' : unreadCount"></span>
|
x-text="unreadCount > 9 ? '9+' : unreadCount"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Messages</h3>
|
<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">Contact support</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.dashboard.messages_card_subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="unreadCount > 0">
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account Summary -->
|
<!-- Account Summary -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700">
|
<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 class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div>
|
<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>
|
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
<div class="mt-8 flex justify-end">
|
<div class="mt-8 flex justify-end">
|
||||||
<button @click="showLogoutModal = true"
|
<button @click="showLogoutModal = true"
|
||||||
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,19 +123,24 @@
|
|||||||
<!-- Logout Confirmation Modal -->
|
<!-- Logout Confirmation Modal -->
|
||||||
{{ confirm_modal(
|
{{ confirm_modal(
|
||||||
id='logoutModal',
|
id='logoutModal',
|
||||||
title='Logout Confirmation',
|
title=_('customers.storefront.pages.dashboard.logout_confirm_title'),
|
||||||
message="Are you sure you want to logout? You'll need to sign in again to access your account.",
|
message=_('customers.storefront.pages.dashboard.logout_confirm_message'),
|
||||||
confirm_action='confirmLogout()',
|
confirm_action='confirmLogout()',
|
||||||
show_var='showLogoutModal',
|
show_var='showLogoutModal',
|
||||||
confirm_text='Logout',
|
confirm_text=_('customers.storefront.pages.dashboard.logout'),
|
||||||
cancel_text='Cancel',
|
cancel_text=_('common.cancel'),
|
||||||
variant='danger'
|
variant='danger'
|
||||||
) }}
|
) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
window.__accountDashboardI18n = {
|
||||||
|
logoutSuccess: {{ _('customers.storefront.pages.dashboard.logout_success')|tojson }},
|
||||||
|
logoutFailed: {{ _('customers.storefront.pages.dashboard.logout_failed')|tojson }},
|
||||||
|
};
|
||||||
function accountDashboard() {
|
function accountDashboard() {
|
||||||
|
const i18n = window.__accountDashboardI18n || {};
|
||||||
return {
|
return {
|
||||||
...storefrontLayoutData(),
|
...storefrontLayoutData(),
|
||||||
showLogoutModal: false,
|
showLogoutModal: false,
|
||||||
@@ -155,7 +161,7 @@ function accountDashboard() {
|
|||||||
localStorage.removeItem('customer_token');
|
localStorage.removeItem('customer_token');
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
this.showToast('Logged out successfully', 'success');
|
this.showToast(i18n.logoutSuccess, 'success');
|
||||||
|
|
||||||
// Redirect to login page
|
// Redirect to login page
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -163,7 +169,7 @@ function accountDashboard() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
console.error('Logout failed with status:', response.status);
|
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)
|
// Still redirect on failure (cookie might be deleted)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '{{ base_url }}account/login';
|
window.location.href = '{{ base_url }}account/login';
|
||||||
@@ -172,7 +178,7 @@ function accountDashboard() {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Logout error:', error);
|
console.error('Logout error:', error);
|
||||||
this.showToast('Logout failed', 'error');
|
this.showToast(i18n.logoutFailed, 'error');
|
||||||
// Redirect anyway
|
// Redirect anyway
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '{{ base_url }}account/login';
|
window.location.href = '{{ base_url }}account/login';
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
<p class="mt-2 text-center">
|
<p class="mt-2 text-center">
|
||||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||||
href="{{ base_url }}">
|
href="{{ base_url }}">
|
||||||
← {{ _("auth.continue_shopping") }}
|
← {{ _("auth.back_to_home") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -180,6 +180,15 @@
|
|||||||
<!-- Alpine.js v3 -->
|
<!-- Alpine.js v3 -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
<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 -->
|
<!-- Forgot Password Logic -->
|
||||||
<script>
|
<script>
|
||||||
function languageSelector(currentLang, enabledLanguages) {
|
function languageSelector(currentLang, enabledLanguages) {
|
||||||
@@ -199,6 +208,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function forgotPassword() {
|
function forgotPassword() {
|
||||||
|
const i18n = window.__forgotPasswordI18n || {};
|
||||||
return {
|
return {
|
||||||
// Data
|
// Data
|
||||||
email: '',
|
email: '',
|
||||||
@@ -240,12 +250,12 @@
|
|||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!this.email) {
|
if (!this.email) {
|
||||||
this.errors.email = 'Email is required';
|
this.errors.email = i18n.emailRequired;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)) {
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)) {
|
||||||
this.errors.email = 'Please enter a valid email address';
|
this.errors.email = i18n.invalidEmail;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +275,7 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
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
|
// Success - show email sent message
|
||||||
@@ -273,7 +283,7 @@
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Forgot password error:', 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 {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
<p class="mt-2 text-center">
|
<p class="mt-2 text-center">
|
||||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||||
href="{{ base_url }}">
|
href="{{ base_url }}">
|
||||||
← {{ _("auth.continue_shopping") }}
|
← {{ _("auth.back_to_home") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{# app/templates/storefront/account/profile.html #}
|
{# app/templates/storefront/account/profile.html #}
|
||||||
{% extends "storefront/base.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 %}
|
{% block alpine_data %}shopProfilePage(){% endblock %}
|
||||||
|
|
||||||
@@ -11,19 +11,19 @@
|
|||||||
<nav class="mb-6" aria-label="Breadcrumb">
|
<nav class="mb-6" aria-label="Breadcrumb">
|
||||||
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<li>
|
<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>
|
||||||
<li class="flex items-center">
|
<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="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>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Profile</h1>
|
<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">Manage your account information and preferences</p>
|
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('customers.storefront.pages.profile.subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@@ -58,15 +58,15 @@
|
|||||||
<!-- Profile Information Section -->
|
<!-- Profile Information Section -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
<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">
|
<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>
|
<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">Update your personal details</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.info_section_subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="saveProfile" class="p-6 space-y-6">
|
<form @submit.prevent="saveProfile" class="p-6 space-y-6">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<!-- First Name -->
|
<!-- First Name -->
|
||||||
<div>
|
<div>
|
||||||
<label for="first_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<input type="text" id="first_name" x-model="profileForm.first_name" required
|
<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
|
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 -->
|
<!-- Last Name -->
|
||||||
<div>
|
<div>
|
||||||
<label for="last_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<input type="text" id="last_name" x-model="profileForm.last_name" required
|
<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
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<input type="email" id="email" x-model="profileForm.email" required
|
<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
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<!-- Phone -->
|
<!-- Phone -->
|
||||||
<div>
|
<div>
|
||||||
<label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Phone Number
|
{{ _('auth.phone_number') }}
|
||||||
</label>
|
</label>
|
||||||
<input type="tel" id="phone" x-model="profileForm.phone"
|
<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
|
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"
|
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
style="background-color: var(--color-primary)">
|
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-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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -131,25 +131,25 @@
|
|||||||
<!-- Preferences Section -->
|
<!-- Preferences Section -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
<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">
|
<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>
|
<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">Manage your account preferences</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.prefs_section_subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="savePreferences" class="p-6 space-y-6">
|
<form @submit.prevent="savePreferences" class="p-6 space-y-6">
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<div>
|
<div>
|
||||||
<label for="language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<select id="language" x-model="preferencesForm.preferred_language"
|
<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
|
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
|
focus:ring-2 focus:ring-primary focus:border-transparent
|
||||||
dark:bg-gray-700 dark:text-white"
|
dark:bg-gray-700 dark:text-white"
|
||||||
style="--tw-ring-color: var(--color-primary)">
|
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="en">English</option>
|
||||||
<option value="fr">Francais</option>
|
<option value="fr">Français</option>
|
||||||
<option value="de">Deutsch</option>
|
<option value="de">Deutsch</option>
|
||||||
<option value="lb">Letzebuergesch</option>
|
<option value="lb">Lëtzebuergesch</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,10 +164,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<label for="marketing_consent" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<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>
|
</label>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
style="background-color: var(--color-primary)">
|
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-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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -191,14 +191,14 @@
|
|||||||
<!-- Change Password Section -->
|
<!-- Change Password Section -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
<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">
|
<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>
|
<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">Update your account password</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ _('customers.storefront.pages.profile.change_password_subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="changePassword" class="p-6 space-y-6">
|
<form @submit.prevent="changePassword" class="p-6 space-y-6">
|
||||||
<!-- Current Password -->
|
<!-- Current Password -->
|
||||||
<div>
|
<div>
|
||||||
<label for="current_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<input type="password" id="current_password" x-model="passwordForm.current_password" required
|
<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
|
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 -->
|
<!-- New Password -->
|
||||||
<div>
|
<div>
|
||||||
<label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<input type="password" id="new_password" x-model="passwordForm.new_password" required
|
<input type="password" id="new_password" x-model="passwordForm.new_password" required
|
||||||
minlength="8"
|
minlength="8"
|
||||||
@@ -219,14 +219,14 @@
|
|||||||
dark:bg-gray-700 dark:text-white"
|
dark:bg-gray-700 dark:text-white"
|
||||||
style="--tw-ring-color: var(--color-primary)">
|
style="--tw-ring-color: var(--color-primary)">
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password -->
|
<!-- Confirm Password -->
|
||||||
<div>
|
<div>
|
||||||
<label for="confirm_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<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>
|
</label>
|
||||||
<input type="password" id="confirm_password" x-model="passwordForm.confirm_password" required
|
<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
|
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)">
|
style="--tw-ring-color: var(--color-primary)">
|
||||||
<p x-show="passwordForm.confirm_password && passwordForm.new_password !== passwordForm.confirm_password"
|
<p x-show="passwordForm.confirm_password && passwordForm.new_password !== passwordForm.confirm_password"
|
||||||
class="mt-1 text-xs text-red-500">
|
class="mt-1 text-xs text-red-500">
|
||||||
Passwords do not match
|
{{ _('auth.passwords_do_not_match') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
style="background-color: var(--color-primary)">
|
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-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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -260,22 +260,22 @@
|
|||||||
|
|
||||||
<!-- Account Info (read-only) -->
|
<!-- 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">
|
<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">
|
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<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>
|
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.customer_number || '-'"></dd>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatDate(profile?.created_at)"></dd>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="profile?.total_orders || 0"></dd>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<dd class="mt-1 text-gray-900 dark:text-white font-medium" x-text="formatPrice(profile?.total_spent)"></dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -286,9 +286,26 @@
|
|||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
<script>
|
<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() {
|
function shopProfilePage() {
|
||||||
|
const i18n = window.__shopProfileI18n || {};
|
||||||
return {
|
return {
|
||||||
...storefrontLayoutData(),
|
...storefrontLayoutData(),
|
||||||
|
i18n,
|
||||||
|
|
||||||
// State
|
// State
|
||||||
profile: null,
|
profile: null,
|
||||||
@@ -347,7 +364,7 @@ function shopProfilePage() {
|
|||||||
window.location.href = '{{ base_url }}account/login?next=' + encodeURIComponent(window.location.pathname);
|
window.location.href = '{{ base_url }}account/login?next=' + encodeURIComponent(window.location.pathname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error('Failed to load profile');
|
throw new Error(i18n.failedToLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.profile = await response.json();
|
this.profile = await response.json();
|
||||||
@@ -366,7 +383,7 @@ function shopProfilePage() {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading profile:', err);
|
console.error('Error loading profile:', err);
|
||||||
this.error = err.message || 'Failed to load profile';
|
this.error = err.message || i18n.failedToLoad;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -395,11 +412,11 @@ function shopProfilePage() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
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.profile = await response.json();
|
||||||
this.successMessage = 'Profile updated successfully';
|
this.successMessage = i18n.profileUpdated;
|
||||||
|
|
||||||
// Update localStorage user data
|
// Update localStorage user data
|
||||||
const userStr = localStorage.getItem('customer_user');
|
const userStr = localStorage.getItem('customer_user');
|
||||||
@@ -415,7 +432,7 @@ function shopProfilePage() {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving profile:', err);
|
console.error('Error saving profile:', err);
|
||||||
this.error = err.message || 'Failed to save profile';
|
this.error = err.message || i18n.failedToSaveProfile;
|
||||||
} finally {
|
} finally {
|
||||||
this.savingProfile = false;
|
this.savingProfile = false;
|
||||||
}
|
}
|
||||||
@@ -444,16 +461,16 @@ function shopProfilePage() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
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.profile = await response.json();
|
||||||
this.successMessage = 'Preferences updated successfully';
|
this.successMessage = i18n.preferencesUpdated;
|
||||||
setTimeout(() => this.successMessage = '', 5000);
|
setTimeout(() => this.successMessage = '', 5000);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving preferences:', err);
|
console.error('Error saving preferences:', err);
|
||||||
this.error = err.message || 'Failed to save preferences';
|
this.error = err.message || i18n.failedToSavePreferences;
|
||||||
} finally {
|
} finally {
|
||||||
this.savingPreferences = false;
|
this.savingPreferences = false;
|
||||||
}
|
}
|
||||||
@@ -461,7 +478,7 @@ function shopProfilePage() {
|
|||||||
|
|
||||||
async changePassword() {
|
async changePassword() {
|
||||||
if (this.passwordForm.new_password !== this.passwordForm.confirm_password) {
|
if (this.passwordForm.new_password !== this.passwordForm.confirm_password) {
|
||||||
this.passwordError = 'Passwords do not match';
|
this.passwordError = i18n.passwordsDoNotMatch;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +504,7 @@ function shopProfilePage() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'Failed to change password');
|
throw new Error(error.detail || i18n.failedToChangePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear password form
|
// Clear password form
|
||||||
@@ -497,12 +514,12 @@ function shopProfilePage() {
|
|||||||
confirm_password: ''
|
confirm_password: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
this.successMessage = 'Password changed successfully';
|
this.successMessage = i18n.passwordChanged;
|
||||||
setTimeout(() => this.successMessage = '', 5000);
|
setTimeout(() => this.successMessage = '', 5000);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error changing password:', err);
|
console.error('Error changing password:', err);
|
||||||
this.passwordError = err.message || 'Failed to change password';
|
this.passwordError = err.message || i18n.failedToChangePassword;
|
||||||
} finally {
|
} finally {
|
||||||
this.changingPassword = false;
|
this.changingPassword = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{# app/templates/storefront/account/reset-password.html #}
|
{# app/templates/storefront/account/reset-password.html #}
|
||||||
{# standalone #}
|
{# standalone #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html :class="{ 'dark': dark }" x-data="resetPassword()" lang="en">
|
<html :class="{ 'dark': dark }" x-data="resetPassword()" lang="{{ current_language|default('fr') }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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 -->
|
<!-- Fonts: Local fallback + Google Fonts -->
|
||||||
<link href="{{ static_v(request, 'static', path='shared/fonts/inter.css') }}" rel="stylesheet" />
|
<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" />
|
<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>
|
<div class="text-6xl mb-4">🔑</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h2 class="text-2xl font-bold text-white mb-2">{{ store.name }}</h2>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,21 +68,22 @@
|
|||||||
<template x-if="tokenInvalid">
|
<template x-if="tokenInvalid">
|
||||||
<div class="text-center">
|
<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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
<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>
|
</h1>
|
||||||
|
|
||||||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
||||||
This password reset link is invalid or has expired.
|
{{ _("auth.invalid_or_expired_link_desc") }}
|
||||||
Please request a new password reset link.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{{ base_url }}account/forgot-password"
|
<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">
|
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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -91,11 +92,11 @@
|
|||||||
<template x-if="!tokenInvalid && !resetComplete">
|
<template x-if="!tokenInvalid && !resetComplete">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||||
Reset Your Password
|
{{ _("auth.reset_your_password") }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
@@ -107,14 +108,14 @@
|
|||||||
<!-- Reset Password Form -->
|
<!-- Reset Password Form -->
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
<label class="block text-sm">
|
<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"
|
<input x-model="password"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@input="clearErrors"
|
@input="clearErrors"
|
||||||
type="password"
|
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="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 }"
|
:class="{ 'border-red-600': errors.password }"
|
||||||
placeholder="Enter new password"
|
placeholder="{{ _('auth.new_password_placeholder') }}"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
required />
|
required />
|
||||||
<span x-show="errors.password" x-text="errors.password"
|
<span x-show="errors.password" x-text="errors.password"
|
||||||
@@ -122,14 +123,14 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="block mt-4 text-sm">
|
<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"
|
<input x-model="confirmPassword"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@input="clearErrors"
|
@input="clearErrors"
|
||||||
type="password"
|
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="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 }"
|
:class="{ 'border-red-600': errors.confirmPassword }"
|
||||||
placeholder="Confirm new password"
|
placeholder="{{ _('auth.confirm_password_placeholder') }}"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
required />
|
required />
|
||||||
<span x-show="errors.confirmPassword" x-text="errors.confirmPassword"
|
<span x-show="errors.confirmPassword" x-text="errors.confirmPassword"
|
||||||
@@ -138,10 +139,13 @@
|
|||||||
|
|
||||||
<button type="submit" :disabled="loading"
|
<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">
|
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 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>
|
<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">
|
||||||
Resetting...
|
<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>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -152,21 +156,22 @@
|
|||||||
<template x-if="resetComplete">
|
<template x-if="resetComplete">
|
||||||
<div class="text-center">
|
<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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||||
Password Reset Complete
|
{{ _("auth.password_reset_complete") }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
|
||||||
Your password has been successfully reset.
|
{{ _("auth.password_reset_success_desc") }}
|
||||||
You can now sign in with your new password.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{{ base_url }}account/login"
|
<a href="{{ base_url }}account/login"
|
||||||
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
|
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
|
||||||
Sign In
|
{{ _("auth.sign_in") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -174,19 +179,34 @@
|
|||||||
<hr class="my-8" />
|
<hr class="my-8" />
|
||||||
|
|
||||||
<p class="mt-4 text-center">
|
<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"
|
<a class="text-sm font-medium hover:underline ml-1"
|
||||||
style="color: var(--color-primary);"
|
style="color: var(--color-primary);"
|
||||||
href="{{ base_url }}account/login">
|
href="{{ base_url }}account/login">
|
||||||
Sign in
|
{{ _("auth.sign_in") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-center">
|
<p class="mt-2 text-center">
|
||||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||||
href="{{ base_url }}">
|
href="{{ base_url }}">
|
||||||
← Continue shopping
|
← {{ _("auth.back_to_home") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,9 +216,37 @@
|
|||||||
<!-- Alpine.js v3 -->
|
<!-- Alpine.js v3 -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
<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 -->
|
<!-- Reset Password Logic -->
|
||||||
<script>
|
<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() {
|
function resetPassword() {
|
||||||
|
const i18n = window.__resetPasswordI18n || {};
|
||||||
return {
|
return {
|
||||||
// Data
|
// Data
|
||||||
token: '',
|
token: '',
|
||||||
@@ -251,22 +299,22 @@
|
|||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!this.password) {
|
if (!this.password) {
|
||||||
this.errors.password = 'Password is required';
|
this.errors.password = i18n.passwordRequired;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.password.length < 8) {
|
if (this.password.length < 8) {
|
||||||
this.errors.password = 'Password must be at least 8 characters';
|
this.errors.password = i18n.passwordTooShort;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.confirmPassword) {
|
if (!this.confirmPassword) {
|
||||||
this.errors.confirmPassword = 'Please confirm your password';
|
this.errors.confirmPassword = i18n.pleaseConfirmPassword;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.password !== this.confirmPassword) {
|
if (this.password !== this.confirmPassword) {
|
||||||
this.errors.confirmPassword = 'Passwords do not match';
|
this.errors.confirmPassword = i18n.passwordsDoNotMatch;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +342,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(data.detail || 'Failed to reset password');
|
throw new Error(data.detail || i18n.resetPasswordFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success
|
// Success
|
||||||
@@ -302,7 +350,7 @@
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Reset password error:', 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 {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ def self_enroll(
|
|||||||
customer_name=data.customer_name,
|
customer_name=data.customer_name,
|
||||||
customer_phone=data.customer_phone,
|
customer_phone=data.customer_phone,
|
||||||
customer_birthday=data.customer_birthday,
|
customer_birthday=data.customer_birthday,
|
||||||
|
customer_language=getattr(request.state, "language", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Self-enrollment for customer {customer_id} at store {store.subdomain}")
|
logger.info(f"Self-enrollment for customer {customer_id} at store {store.subdomain}")
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ class CardService:
|
|||||||
customer_name: str | None = None,
|
customer_name: str | None = None,
|
||||||
customer_phone: str | None = None,
|
customer_phone: str | None = None,
|
||||||
customer_birthday: date | None = None,
|
customer_birthday: date | None = None,
|
||||||
|
customer_language: str | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Resolve a customer ID from either a direct ID or email lookup.
|
Resolve a customer ID from either a direct ID or email lookup.
|
||||||
@@ -224,11 +225,18 @@ class CardService:
|
|||||||
|
|
||||||
customer = customer_service.get_customer_by_email(db, store_id, email)
|
customer = customer_service.get_customer_by_email(db, store_id, email)
|
||||||
if customer:
|
if customer:
|
||||||
# Backfill birthday on existing customer if they didn't have
|
# Backfill birthday + preferred_language on existing customer
|
||||||
# one before — keeps the enrollment form useful for returning
|
# if they were missing — keeps the enrollment form useful for
|
||||||
# customers who never previously provided a birthday.
|
# returning customers and lets transactional emails (welcome,
|
||||||
|
# password reset) hit the right locale.
|
||||||
|
dirty = False
|
||||||
if customer_birthday and not customer.birth_date:
|
if customer_birthday and not customer.birth_date:
|
||||||
customer.birth_date = customer_birthday
|
customer.birth_date = customer_birthday
|
||||||
|
dirty = True
|
||||||
|
if customer_language and not customer.preferred_language:
|
||||||
|
customer.preferred_language = customer_language
|
||||||
|
dirty = True
|
||||||
|
if dirty:
|
||||||
db.flush()
|
db.flush()
|
||||||
return customer.id
|
return customer.id
|
||||||
|
|
||||||
@@ -250,8 +258,17 @@ class CardService:
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if existing_cardholder:
|
if existing_cardholder:
|
||||||
|
dirty = False
|
||||||
if customer_birthday and not existing_cardholder.birth_date:
|
if customer_birthday and not existing_cardholder.birth_date:
|
||||||
existing_cardholder.birth_date = customer_birthday
|
existing_cardholder.birth_date = customer_birthday
|
||||||
|
dirty = True
|
||||||
|
if (
|
||||||
|
customer_language
|
||||||
|
and not existing_cardholder.preferred_language
|
||||||
|
):
|
||||||
|
existing_cardholder.preferred_language = customer_language
|
||||||
|
dirty = True
|
||||||
|
if dirty:
|
||||||
db.flush()
|
db.flush()
|
||||||
return existing_cardholder.id
|
return existing_cardholder.id
|
||||||
|
|
||||||
@@ -272,6 +289,7 @@ class CardService:
|
|||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
phone=customer_phone,
|
phone=customer_phone,
|
||||||
birth_date=customer_birthday,
|
birth_date=customer_birthday,
|
||||||
|
preferred_language=customer_language,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Created customer {customer.id} ({email}) "
|
f"Created customer {customer.id} ({email}) "
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"visit_platform": "Besuchen Sie unsere Plattform",
|
"visit_platform": "Besuchen Sie unsere Plattform",
|
||||||
"already_have_account": "Haben Sie bereits ein Konto?",
|
"already_have_account": "Haben Sie bereits ein Konto?",
|
||||||
"create_account": "Konto erstellen",
|
"create_account": "Konto erstellen",
|
||||||
"continue_shopping": "Weiter einkaufen",
|
"back_to_home": "Zurück zur Startseite",
|
||||||
"admin_login": "Admin-Anmeldung",
|
"admin_login": "Admin-Anmeldung",
|
||||||
"merchant_login": "Händler-Anmeldung",
|
"merchant_login": "Händler-Anmeldung",
|
||||||
"store_login": "Shop-Portal-Anmeldung",
|
"store_login": "Shop-Portal-Anmeldung",
|
||||||
@@ -140,7 +140,27 @@
|
|||||||
"reset_link_sent": "Wir haben einen Link zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang und klicken Sie auf den Link.",
|
"reset_link_sent": "Wir haben einen Link zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang und klicken Sie auf den Link.",
|
||||||
"didnt_receive_email": "E-Mail nicht erhalten? Überprüfen Sie Ihren Spam-Ordner oder",
|
"didnt_receive_email": "E-Mail nicht erhalten? Überprüfen Sie Ihren Spam-Ordner oder",
|
||||||
"try_again": "versuchen Sie es erneut",
|
"try_again": "versuchen Sie es erneut",
|
||||||
"remember_password": "Passwort wieder eingefallen?"
|
"remember_password": "Passwort wieder eingefallen?",
|
||||||
|
"reset_password_subtitle": "Neues Passwort erstellen",
|
||||||
|
"reset_your_password": "Passwort zurücksetzen",
|
||||||
|
"reset_password_form_desc": "Geben Sie unten Ihr neues Passwort ein. Es muss mindestens 8 Zeichen lang sein.",
|
||||||
|
"new_password_placeholder": "Neues Passwort eingeben",
|
||||||
|
"confirm_password_placeholder": "Neues Passwort bestätigen",
|
||||||
|
"resetting": "Wird zurückgesetzt...",
|
||||||
|
"reset_password_btn": "Passwort zurücksetzen",
|
||||||
|
"password_reset_complete": "Passwort zurückgesetzt",
|
||||||
|
"password_reset_success_desc": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit dem neuen Passwort anmelden.",
|
||||||
|
"invalid_or_expired_link": "Ungültiger oder abgelaufener Link",
|
||||||
|
"invalid_or_expired_link_desc": "Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.",
|
||||||
|
"request_new_link": "Neuen Link anfordern",
|
||||||
|
"email_required": "E-Mail ist erforderlich",
|
||||||
|
"invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||||
|
"forgot_password_failed": "Link konnte nicht gesendet werden. Bitte erneut versuchen.",
|
||||||
|
"password_required": "Passwort ist erforderlich",
|
||||||
|
"password_too_short": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
||||||
|
"please_confirm_password": "Bitte bestätigen Sie Ihr Passwort",
|
||||||
|
"passwords_do_not_match": "Passwörter stimmen nicht überein",
|
||||||
|
"reset_password_failed": "Passwort konnte nicht zurückgesetzt werden. Bitte erneut versuchen."
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"visit_platform": "Visit our platform",
|
"visit_platform": "Visit our platform",
|
||||||
"already_have_account": "Already have an account?",
|
"already_have_account": "Already have an account?",
|
||||||
"create_account": "Create an account",
|
"create_account": "Create an account",
|
||||||
"continue_shopping": "Continue shopping",
|
"back_to_home": "Back to Home",
|
||||||
"admin_login": "Admin Login",
|
"admin_login": "Admin Login",
|
||||||
"merchant_login": "Merchant Login",
|
"merchant_login": "Merchant Login",
|
||||||
"store_login": "Store Portal Login",
|
"store_login": "Store Portal Login",
|
||||||
@@ -140,7 +140,27 @@
|
|||||||
"reset_link_sent": "We've sent a password reset link to your email. Please check your inbox and click the link to reset your password.",
|
"reset_link_sent": "We've sent a password reset link to your email. Please check your inbox and click the link to reset your password.",
|
||||||
"didnt_receive_email": "Didn't receive the email? Check your spam folder or",
|
"didnt_receive_email": "Didn't receive the email? Check your spam folder or",
|
||||||
"try_again": "try again",
|
"try_again": "try again",
|
||||||
"remember_password": "Remember your password?"
|
"remember_password": "Remember your password?",
|
||||||
|
"reset_password_subtitle": "Create new password",
|
||||||
|
"reset_your_password": "Reset Your Password",
|
||||||
|
"reset_password_form_desc": "Enter your new password below. Password must be at least 8 characters.",
|
||||||
|
"new_password_placeholder": "Enter new password",
|
||||||
|
"confirm_password_placeholder": "Confirm new password",
|
||||||
|
"resetting": "Resetting...",
|
||||||
|
"reset_password_btn": "Reset Password",
|
||||||
|
"password_reset_complete": "Password Reset Complete",
|
||||||
|
"password_reset_success_desc": "Your password has been successfully reset. You can now sign in with your new password.",
|
||||||
|
"invalid_or_expired_link": "Invalid or Expired Link",
|
||||||
|
"invalid_or_expired_link_desc": "This password reset link is invalid or has expired. Please request a new password reset link.",
|
||||||
|
"request_new_link": "Request New Link",
|
||||||
|
"email_required": "Email is required",
|
||||||
|
"invalid_email": "Please enter a valid email address",
|
||||||
|
"forgot_password_failed": "Failed to send reset link. Please try again.",
|
||||||
|
"password_required": "Password is required",
|
||||||
|
"password_too_short": "Password must be at least 8 characters",
|
||||||
|
"please_confirm_password": "Please confirm your password",
|
||||||
|
"passwords_do_not_match": "Passwords do not match",
|
||||||
|
"reset_password_failed": "Failed to reset password. Please try again."
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"visit_platform": "Visitez notre plateforme",
|
"visit_platform": "Visitez notre plateforme",
|
||||||
"already_have_account": "Vous avez déjà un compte ?",
|
"already_have_account": "Vous avez déjà un compte ?",
|
||||||
"create_account": "Créer un compte",
|
"create_account": "Créer un compte",
|
||||||
"continue_shopping": "Continuer vos achats",
|
"back_to_home": "Retour à l'accueil",
|
||||||
"admin_login": "Connexion Admin",
|
"admin_login": "Connexion Admin",
|
||||||
"merchant_login": "Connexion Marchand",
|
"merchant_login": "Connexion Marchand",
|
||||||
"store_login": "Connexion Portail Magasin",
|
"store_login": "Connexion Portail Magasin",
|
||||||
@@ -140,7 +140,27 @@
|
|||||||
"reset_link_sent": "Nous avons envoyé un lien de réinitialisation à votre adresse e-mail. Veuillez vérifier votre boîte de réception et cliquer sur le lien.",
|
"reset_link_sent": "Nous avons envoyé un lien de réinitialisation à votre adresse e-mail. Veuillez vérifier votre boîte de réception et cliquer sur le lien.",
|
||||||
"didnt_receive_email": "Vous n'avez pas reçu l'e-mail ? Vérifiez votre dossier spam ou",
|
"didnt_receive_email": "Vous n'avez pas reçu l'e-mail ? Vérifiez votre dossier spam ou",
|
||||||
"try_again": "réessayez",
|
"try_again": "réessayez",
|
||||||
"remember_password": "Vous vous souvenez de votre mot de passe ?"
|
"remember_password": "Vous vous souvenez de votre mot de passe ?",
|
||||||
|
"reset_password_subtitle": "Créer un nouveau mot de passe",
|
||||||
|
"reset_your_password": "Réinitialisez votre mot de passe",
|
||||||
|
"reset_password_form_desc": "Saisissez votre nouveau mot de passe ci-dessous. Il doit comporter au moins 8 caractères.",
|
||||||
|
"new_password_placeholder": "Saisissez le nouveau mot de passe",
|
||||||
|
"confirm_password_placeholder": "Confirmez le nouveau mot de passe",
|
||||||
|
"resetting": "Réinitialisation...",
|
||||||
|
"reset_password_btn": "Réinitialiser le mot de passe",
|
||||||
|
"password_reset_complete": "Mot de passe réinitialisé",
|
||||||
|
"password_reset_success_desc": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.",
|
||||||
|
"invalid_or_expired_link": "Lien invalide ou expiré",
|
||||||
|
"invalid_or_expired_link_desc": "Ce lien de réinitialisation est invalide ou a expiré. Veuillez en demander un nouveau.",
|
||||||
|
"request_new_link": "Demander un nouveau lien",
|
||||||
|
"email_required": "L'e-mail est obligatoire",
|
||||||
|
"invalid_email": "Veuillez saisir une adresse e-mail valide",
|
||||||
|
"forgot_password_failed": "Échec de l'envoi du lien. Veuillez réessayer.",
|
||||||
|
"password_required": "Le mot de passe est obligatoire",
|
||||||
|
"password_too_short": "Le mot de passe doit contenir au moins 8 caractères",
|
||||||
|
"please_confirm_password": "Veuillez confirmer votre mot de passe",
|
||||||
|
"passwords_do_not_match": "Les mots de passe ne correspondent pas",
|
||||||
|
"reset_password_failed": "Échec de la réinitialisation. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Tableau de bord",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"visit_platform": "Besicht eis Plattform",
|
"visit_platform": "Besicht eis Plattform",
|
||||||
"already_have_account": "Hutt Dir schonn e Kont?",
|
"already_have_account": "Hutt Dir schonn e Kont?",
|
||||||
"create_account": "E Kont erstellen",
|
"create_account": "E Kont erstellen",
|
||||||
"continue_shopping": "Weider akafen",
|
"back_to_home": "Zréck op d'Haaptsäit",
|
||||||
"admin_login": "Admin Login",
|
"admin_login": "Admin Login",
|
||||||
"merchant_login": "Händler Login",
|
"merchant_login": "Händler Login",
|
||||||
"store_login": "Buttek-Portal Login",
|
"store_login": "Buttek-Portal Login",
|
||||||
@@ -140,7 +140,27 @@
|
|||||||
"reset_link_sent": "Mir hunn e Link fir d'Passwuert zréckzesetzen op Är E-Mail-Adress geschéckt. Kuckt w.e.g. Ären Posteingang a klickt op de Link.",
|
"reset_link_sent": "Mir hunn e Link fir d'Passwuert zréckzesetzen op Är E-Mail-Adress geschéckt. Kuckt w.e.g. Ären Posteingang a klickt op de Link.",
|
||||||
"didnt_receive_email": "E-Mail net kritt? Kuckt Ären Spam-Dossier oder",
|
"didnt_receive_email": "E-Mail net kritt? Kuckt Ären Spam-Dossier oder",
|
||||||
"try_again": "probéiert et nach eng Kéier",
|
"try_again": "probéiert et nach eng Kéier",
|
||||||
"remember_password": "Passwuert erëm agefall?"
|
"remember_password": "Passwuert erëm agefall?",
|
||||||
|
"reset_password_subtitle": "Neit Passwuert erstellen",
|
||||||
|
"reset_your_password": "Ärt Passwuert zrécksetzen",
|
||||||
|
"reset_password_form_desc": "Gitt hei drënner Ärt neit Passwuert an. Et muss mindestens 8 Zeechen laang sinn.",
|
||||||
|
"new_password_placeholder": "Neit Passwuert agi",
|
||||||
|
"confirm_password_placeholder": "Neit Passwuert bestätegen",
|
||||||
|
"resetting": "Gëtt zréckgesat...",
|
||||||
|
"reset_password_btn": "Passwuert zrécksetzen",
|
||||||
|
"password_reset_complete": "Passwuert zréckgesat",
|
||||||
|
"password_reset_success_desc": "Ärt Passwuert ass erfollegräich zréckgesat. Dir kënnt Iech elo mat Ärem neie Passwuert aloggen.",
|
||||||
|
"invalid_or_expired_link": "Ongëltege oder ofgelafenen Link",
|
||||||
|
"invalid_or_expired_link_desc": "Dëse Link fir d'Passwuert zréckzesetzen ass ongëlteg oder ofgelaf. Bitt e neie Link un.",
|
||||||
|
"request_new_link": "Neie Link ufroen",
|
||||||
|
"email_required": "E-Mail ass obligatoresch",
|
||||||
|
"invalid_email": "Gitt w.e.g. eng gëlteg E-Mail-Adress un",
|
||||||
|
"forgot_password_failed": "Link konnt net geschéckt ginn. Probéiert nach eng Kéier.",
|
||||||
|
"password_required": "Passwuert ass obligatoresch",
|
||||||
|
"password_too_short": "D'Passwuert muss mindestens 8 Zeechen hunn",
|
||||||
|
"please_confirm_password": "Bestätegt w.e.g. Ärt Passwuert",
|
||||||
|
"passwords_do_not_match": "D'Passwierder stëmmen net iwwerteneen",
|
||||||
|
"reset_password_failed": "Passwuert konnt net zréckgesat ginn. Probéiert nach eng Kéier."
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
|||||||
Reference in New Issue
Block a user