feat: add customer authentication pages and documentation

Add complete customer authentication UI with login, registration,
forgot password, and dashboard pages.

Templates Added:
- app/templates/shop/account/login.html
  - Two-column layout with vendor branding
  - Email/password login with validation
  - Password visibility toggle
  - "Remember me" functionality
  - Error/success alerts
  - Loading states with spinner
- app/templates/shop/account/register.html
  - Customer registration form
  - Client-side validation (password strength, email format)
  - Marketing consent checkbox
  - Confirm password matching
- app/templates/shop/account/forgot-password.html
  - Password reset request page
  - Email validation
  - Success confirmation
- app/templates/shop/account/dashboard.html
  - Customer account dashboard
  - Overview of orders, profile, addresses

Styles Added:
- static/shared/css/auth.css
  - Authentication page styling
  - Two-column layout system
  - Form components and validation states
  - Theme-aware with CSS variables
  - Dark mode support
  - Mobile responsive
- static/shared/css/base.css updates
  - Enhanced utility classes
  - Additional form styles
  - Improved button states

Documentation Added:
- docs/frontend/shop/authentication-pages.md
  - Comprehensive guide to auth page implementation
  - Component architecture
  - API integration patterns
  - Theme customization
- docs/development/CUSTOMER_AUTHENTICATION_IMPLEMENTATION.md
  - Implementation details and technical decisions
  - Security considerations
  - Testing procedures
- docs/development/CUSTOMER_AUTH_SUMMARY.md
  - Quick reference guide
  - Endpoints and flows
- Updated docs/frontend/shop/architecture.md
  - Added authentication section
  - Documented all auth pages
- Updated docs/frontend/shop/page-templates.md
  - Added auth template documentation
- Updated mkdocs.yml
  - Added new documentation pages to navigation

Features:
- Full theme integration with vendor branding
- Alpine.js reactive components
- Tailwind CSS utility-first styling
- Client and server-side validation
- JWT token management
- Multi-access routing support (domain/subdomain/path)
- Error handling with user-friendly messages
- Loading states and animations
- Mobile responsive design
- Dark mode support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 21:09:27 +01:00
parent 6735d99df2
commit 86d67b5cfb
12 changed files with 3118 additions and 473 deletions

View File

@@ -0,0 +1,228 @@
{# app/templates/shop/account/dashboard.html #}
{% extends "shop/base.html" %}
{% block title %}My Account - {{ vendor.name }}{% endblock %}
{% block alpine_data %}accountDashboard(){% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">My Account</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Welcome back, {{ user.first_name }}!</p>
</div>
<!-- Dashboard Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- Orders Card -->
<a href="{{ base_url }}shop/account/orders"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Orders</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">View order history</p>
</div>
</div>
<div>
<p class="text-2xl font-bold text-primary" style="color: var(--color-primary)">{{ user.total_orders }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Total Orders</p>
</div>
</a>
<!-- Profile Card -->
<a href="{{ base_url }}shop/account/profile"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Profile</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Edit your information</p>
</div>
</div>
<div>
<p class="text-sm text-gray-700 dark:text-gray-300 truncate">{{ user.email }}</p>
</div>
</a>
<!-- Addresses Card -->
<a href="{{ base_url }}shop/account/addresses"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-primary" style="color: var(--color-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Addresses</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Manage addresses</p>
</div>
</div>
</a>
</div>
<!-- Account Summary -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Account Summary</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Since</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.created_at.strftime('%B %Y') }}</p>
</div>
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Orders</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.total_orders }}</p>
</div>
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Customer Number</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white">{{ user.customer_number }}</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="mt-8 flex justify-end">
<button @click="showLogoutModal = true"
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Logout
</button>
</div>
</div>
<!-- Logout Confirmation Modal -->
<div x-show="showLogoutModal"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<!-- Background overlay -->
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<!-- Overlay backdrop -->
<div x-show="showLogoutModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showLogoutModal = false"
class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
aria-hidden="true">
</div>
<!-- Center modal -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- Modal panel -->
<div x-show="showLogoutModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<!-- Icon -->
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<!-- Content -->
<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" id="modal-title">
Logout Confirmation
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Are you sure you want to logout? You'll need to sign in again to access your account.
</p>
</div>
</div>
</div>
<!-- Actions -->
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button @click="confirmLogout()"
type="button"
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 focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm transition-colors">
Logout
</button>
<button @click="showLogoutModal = false"
type="button"
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm transition-colors">
Cancel
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function accountDashboard() {
return {
...shopLayoutData(),
showLogoutModal: false,
confirmLogout() {
// Close modal
this.showLogoutModal = false;
fetch('/api/v1/shop/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
if (response.ok) {
// Clear localStorage token if any
localStorage.removeItem('customer_token');
// Show success message
this.showToast('Logged out successfully', 'success');
// Redirect to login page
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
}, 500);
} else {
console.error('Logout failed with status:', response.status);
this.showToast('Logout failed', 'error');
// Still redirect on failure (cookie might be deleted)
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
}, 1000);
}
})
.catch(error => {
console.error('Logout error:', error);
this.showToast('Logout failed', 'error');
// Redirect anyway
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
}, 1000);
});
}
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,254 @@
{# app/templates/shop/account/forgot-password.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="forgotPassword()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forgot Password - {{ vendor.name }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
{# CRITICAL: Inject theme CSS variables #}
<style id="vendor-theme-variables">
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
{# Custom CSS from vendor theme #}
{% if theme.custom_css %}
{{ theme.custom_css | safe }}
{% endif %}
/* Theme-aware button and focus colors */
.btn-primary-theme {
background-color: var(--color-primary);
}
.btn-primary-theme:hover:not(:disabled) {
background-color: var(--color-primary-dark, var(--color-primary));
filter: brightness(0.9);
}
.focus-primary:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 124, 58, 237), 0.1);
}
[x-cloak] { display: none !important; }
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex flex-col overflow-y-auto md:flex-row">
<!-- Left side - Image/Branding with Theme Colors -->
<div class="h-32 md:h-auto md:w-1/2 flex items-center justify-center"
style="background-color: var(--color-primary);">
<div class="text-center p-8">
{% if theme.branding.logo %}
<img src="{{ theme.branding.logo }}"
alt="{{ vendor.name }}"
class="mx-auto mb-4 max-w-xs max-h-32 object-contain" />
{% else %}
<div class="text-6xl mb-4">🔐</div>
{% endif %}
<h2 class="text-2xl font-bold text-white mb-2">{{ vendor.name }}</h2>
<p class="text-white opacity-90">Reset your password</p>
</div>
</div>
<!-- Right side - Forgot Password Form -->
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<!-- Initial Form State -->
<template x-if="!emailSent">
<div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Forgot Password
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
Enter your email address and we'll send you a link to reset your password.
</p>
<!-- Error Message -->
<div x-show="alert.show && alert.type === 'error'"
x-text="alert.message"
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
x-transition></div>
<!-- Forgot Password Form -->
<form @submit.prevent="handleSubmit">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Email Address</span>
<input x-model="email"
:disabled="loading"
@input="clearErrors"
type="email"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.email }"
placeholder="your@email.com"
autocomplete="email"
required />
<span x-show="errors.email" x-text="errors.email"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Send Reset Link</span>
<span x-show="loading" class="flex items-center justify-center">
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</span>
</button>
</form>
</div>
</template>
<!-- Success State -->
<template x-if="emailSent">
<div class="text-center">
<div class="flex items-center justify-center w-16 h-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900">
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Check Your Email
</h1>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
We've sent a password reset link to <strong x-text="email"></strong>.
Please check your inbox and click the link to reset your password.
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
Didn't receive the email? Check your spam folder or
<button @click="emailSent = false"
class="font-medium hover:underline"
style="color: var(--color-primary);">
try again
</button>
</p>
</div>
</template>
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
Sign in
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
← Continue shopping
</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
<!-- Forgot Password Logic -->
<script>
function forgotPassword() {
return {
// Data
email: '',
emailSent: false,
loading: false,
errors: {},
alert: {
show: false,
type: 'error',
message: ''
},
dark: false,
// Initialize
init() {
// Check for dark mode preference
this.dark = localStorage.getItem('darkMode') === 'true';
},
// Clear errors
clearErrors() {
this.errors = {};
this.alert.show = false;
},
// Show alert
showAlert(message, type = 'error') {
this.alert = {
show: true,
type: type,
message: message
};
window.scrollTo({ top: 0, behavior: 'smooth' });
},
// Handle form submission
async handleSubmit() {
this.clearErrors();
// Basic validation
if (!this.email) {
this.errors.email = 'Email is required';
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)) {
this.errors.email = 'Please enter a valid email address';
return;
}
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: this.email
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Failed to send reset link');
}
// Success - show email sent message
this.emailSent = true;
} catch (error) {
console.error('Forgot password error:', error);
this.showAlert(error.message || 'Failed to send reset link. Please try again.');
} finally {
this.loading = false;
}
}
}
}
</script>
</body>
</html>

View File

@@ -1,127 +1,177 @@
{# app/templates/shop/account/login.html #}
<!DOCTYPE html>
<html lang="en">
<html :class="{ 'theme-dark': dark }" x-data="customerLogin()" lang="en">
<head>
<meta charset="UTF-8">
<title><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - {{ vendor.name }}</title>
<link rel="stylesheet" href="/static/css/shared/base.css">
<link rel="stylesheet" href="/static/css/shared/auth.css">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Customer Login - {{ vendor.name }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
{# CRITICAL: Inject theme CSS variables #}
<style id="vendor-theme-variables">
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
{# Custom CSS from vendor theme #}
{% if theme.custom_css %}
{{ theme.custom_css | safe }}
{% endif %}
/* Theme-aware button and focus colors */
.btn-primary-theme {
background-color: var(--color-primary);
}
.btn-primary-theme:hover:not(:disabled) {
background-color: var(--color-primary-dark, var(--color-primary));
filter: brightness(0.9);
}
.focus-primary:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 124, 58, 237), 0.1);
}
[x-cloak] { display: none !important; }
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
</head>
<body class="auth-page">
<div class="login-container"
x-data="customerLogin()"
x-init="checkRegistrationSuccess()"
data-vendor-id="{{ vendor.id }}"
data-vendor-name="{{ vendor.name }}"
>
<!-- Header -->
<div class="login-header">
{% if vendor.logo_url %}
<img src="{{ vendor.logo_url }}" alt="{{ vendor.name }}" class="auth-logo">
{% else %}
<div class="auth-logo">🛒</div>
{% endif %}
<h1>Welcome Back</h1>
<p>Sign in to {{ vendor.name }}</p>
</div>
<!-- Alert Box -->
<div x-show="alert.show"
x-transition
:class="'alert alert-' + alert.type"
x-text="alert.message"
></div>
<!-- Login Form -->
<form @submit.prevent="handleLogin">
<!-- Email -->
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
x-model="credentials.email"
required
placeholder="your@email.com"
:class="{ 'error': errors.email }"
@input="clearAllErrors()"
>
<div x-show="errors.email"
x-text="errors.email"
class="error-message show"
></div>
</div>
<!-- Password -->
<div class="form-group">
<label for="password">Password</label>
<div class="password-group">
<input
:type="showPassword ? 'text' : 'password'"
id="password"
x-model="credentials.password"
required
placeholder="Enter your password"
:class="{ 'error': errors.password }"
@input="clearAllErrors()"
>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
>
<span x-text="showPassword ? '👁️' : '👁️‍🗨️'"></span>
</button>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex flex-col overflow-y-auto md:flex-row">
<!-- Left side - Image/Branding with Theme Colors -->
<div class="h-32 md:h-auto md:w-1/2 flex items-center justify-center"
style="background-color: var(--color-primary);">
<div class="text-center p-8">
{% if theme.branding.logo %}
<img src="{{ theme.branding.logo }}"
alt="{{ vendor.name }}"
class="mx-auto mb-4 max-w-xs max-h-32 object-contain" />
{% else %}
<div class="text-6xl mb-4">🛒</div>
{% endif %}
<h2 class="text-2xl font-bold text-white mb-2">{{ vendor.name }}</h2>
<p class="text-white opacity-90">Welcome back to your shopping experience</p>
</div>
</div>
<div x-show="errors.password"
x-text="errors.password"
class="error-message show"
></div>
</div>
<!-- Remember Me & Forgot Password -->
<div class="form-options">
<div class="remember-me">
<input
type="checkbox"
id="rememberMe"
x-model="rememberMe"
>
<label for="rememberMe">Remember me</label>
<!-- Right side - Login Form -->
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Customer Login
</h1>
<!-- Success Message (after registration) -->
<div x-show="alert.show && alert.type === 'success'"
x-text="alert.message"
class="px-4 py-3 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
x-transition></div>
<!-- Error Message -->
<div x-show="alert.show && alert.type === 'error'"
x-text="alert.message"
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
x-transition></div>
<!-- Login Form -->
<form @submit.prevent="handleLogin">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Email Address</span>
<input x-model="credentials.email"
:disabled="loading"
@input="clearAllErrors"
type="email"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.email }"
placeholder="your@email.com"
autocomplete="email"
required />
<span x-show="errors.email" x-text="errors.email"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Password</span>
<div class="relative">
<input x-model="credentials.password"
:disabled="loading"
@input="clearAllErrors"
:type="showPassword ? 'text' : 'password'"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.password }"
placeholder="Enter your password"
autocomplete="current-password"
required />
<button type="button"
@click="showPassword = !showPassword"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span x-text="showPassword ? '👁️' : '👁️‍🗨️'"></span>
</button>
</div>
<span x-show="errors.password" x-text="errors.password"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between mt-4">
<label class="flex items-center text-sm">
<input type="checkbox"
x-model="rememberMe"
class="form-checkbox focus-primary focus:outline-none"
style="color: var(--color-primary);">
<span class="ml-2 text-gray-700 dark:text-gray-400">Remember me</span>
</label>
<a href="{{ base_url }}shop/account/forgot-password"
class="text-sm font-medium hover:underline"
style="color: var(--color-primary);">
Forgot password?
</a>
</div>
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Sign in</span>
<span x-show="loading" class="flex items-center justify-center">
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Signing in...
</span>
</button>
</form>
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Don't have an account?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/register">
Create an account
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
← Continue shopping
</a>
</p>
</div>
</div>
<a href="{{ base_url }}shop/account/forgot-password" class="forgot-password">
Forgot password?
</a>
</div>
<!-- Submit Button -->
<button
type="submit"
class="btn-login"
:disabled="loading"
>
<span x-show="loading" class="loading-spinner"></span>
<span x-text="loading ? 'Signing in...' : 'Sign In'"></span>
</button>
</form>
<!-- Register Link -->
<div class="login-footer">
<div class="auth-footer-text">Don't have an account?</div>
<a href="{{ base_url }}shop/account/register">Create an account</a>
</div>
<!-- Back to Shop -->
<div class="login-footer" style="border-top: none; padding-top: 0;">
<a href="{{ base_url }}shop/">← Continue shopping</a>
</div>
</div>
<!-- Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
<!-- Login Logic -->
<script>
function customerLogin() {
return {
@@ -139,33 +189,29 @@
type: 'error',
message: ''
},
// Get vendor data
get vendorId() {
return this.$el.dataset.vendorId;
dark: false,
// Initialize
init() {
this.checkRegistrationSuccess();
// Check for dark mode preference
this.dark = localStorage.getItem('darkMode') === 'true';
},
get vendorName() {
return this.$el.dataset.vendorName;
},
// Check if redirected after registration
checkRegistrationSuccess() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('registered') === 'true') {
this.showAlert(
'Account created successfully! Please sign in.',
'success'
);
this.showAlert('Account created successfully! Please sign in.', 'success');
}
},
// Clear errors
clearAllErrors() {
this.errors = {};
this.alert.show = false;
},
// Show alert
showAlert(message, type = 'error') {
this.alert = {
@@ -173,63 +219,56 @@
type: type,
message: message
};
window.scrollTo({ top: 0, behavior: 'smooth' });
},
// Handle login
async handleLogin() {
this.clearAllErrors();
// Basic validation
if (!this.credentials.email) {
this.errors.email = 'Email is required';
return;
}
if (!this.credentials.password) {
this.errors.password = 'Password is required';
return;
}
this.loading = true;
try {
const response = await fetch(
`/api/v1/shop/auth/login`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email_or_username: this.credentials.email,
password: this.credentials.password
})
}
);
const response = await fetch('/api/v1/shop/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email_or_username: this.credentials.email,
password: this.credentials.password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Login failed');
}
// Store token and user data
localStorage.setItem('customer_token', data.access_token);
localStorage.setItem('customer_user', JSON.stringify(data.user));
// Store vendor context
localStorage.setItem('customer_vendor_id', this.vendorId);
this.showAlert('Login successful! Redirecting...', 'success');
// Redirect to account page or cart
// Redirect to account page or return URL
setTimeout(() => {
const returnUrl = new URLSearchParams(window.location.search).get('return') || '/shop/account';
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}shop/account';
window.location.href = returnUrl;
}, 1000);
} catch (error) {
console.error('Login error:', error);
this.showAlert(error.message || 'Invalid email or password');
@@ -241,9 +280,4 @@
}
</script>
</body>
</html></title>
</head>
<body>
</body>
</html>
</html>

View File

@@ -1,341 +1,378 @@
{# app/templates/shop/account/register.html #}
<!DOCTYPE html>
<html lang="en">
<html :class="{ 'theme-dark': dark }" x-data="customerRegistration()" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create Account - {{ vendor.name }}</title>
<link rel="stylesheet" href="/static/css/shared/base.css">
<link rel="stylesheet" href="/static/css/shared/auth.css">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="auth-page">
<div class="login-container"
x-data="customerRegistration()"
data-vendor-id="{{ vendor.id }}"
data-vendor-name="{{ vendor.name }}"
>
<!-- Header -->
<div class="login-header">
{% if vendor.logo_url %}
<img src="{{ vendor.logo_url }}" alt="{{ vendor.name }}" class="auth-logo">
{% else %}
<div class="auth-logo">🛒</div>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
{# CRITICAL: Inject theme CSS variables #}
<style id="vendor-theme-variables">
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
{# Custom CSS from vendor theme #}
{% if theme.custom_css %}
{{ theme.custom_css | safe }}
{% endif %}
<h1>Create Account</h1>
<p>Join {{ vendor.name }}</p>
/* Theme-aware button and focus colors */
.btn-primary-theme {
background-color: var(--color-primary);
}
.btn-primary-theme:hover:not(:disabled) {
background-color: var(--color-primary-dark, var(--color-primary));
filter: brightness(0.9);
}
.focus-primary:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 124, 58, 237), 0.1);
}
[x-cloak] { display: none !important; }
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex flex-col overflow-y-auto md:flex-row">
<!-- Left side - Image/Branding with Theme Colors -->
<div class="h-32 md:h-auto md:w-1/2 flex items-center justify-center"
style="background-color: var(--color-primary);">
<div class="text-center p-8">
{% if theme.branding.logo %}
<img src="{{ theme.branding.logo }}"
alt="{{ vendor.name }}"
class="mx-auto mb-4 max-w-xs max-h-32 object-contain" />
{% else %}
<div class="text-6xl mb-4">🛒</div>
{% endif %}
<h2 class="text-2xl font-bold text-white mb-2">{{ vendor.name }}</h2>
<p class="text-white opacity-90">Join our community today</p>
</div>
</div>
<!-- Right side - Registration Form -->
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Create Account
</h1>
<!-- Success Message -->
<div x-show="alert.show && alert.type === 'success'"
x-text="alert.message"
class="px-4 py-3 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
x-transition></div>
<!-- Error Message -->
<div x-show="alert.show && alert.type === 'error'"
x-text="alert.message"
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
x-transition></div>
<!-- Registration Form -->
<form @submit.prevent="handleRegister">
<!-- First Name -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
First Name <span class="text-red-600">*</span>
</span>
<input x-model="formData.first_name"
:disabled="loading"
@input="clearError('first_name')"
type="text"
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.first_name }"
placeholder="Enter your first name"
required />
<span x-show="errors.first_name" x-text="errors.first_name"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<!-- Last Name -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Last Name <span class="text-red-600">*</span>
</span>
<input x-model="formData.last_name"
:disabled="loading"
@input="clearError('last_name')"
type="text"
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.last_name }"
placeholder="Enter your last name"
required />
<span x-show="errors.last_name" x-text="errors.last_name"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<!-- Email -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Email Address <span class="text-red-600">*</span>
</span>
<input x-model="formData.email"
:disabled="loading"
@input="clearError('email')"
type="email"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.email }"
placeholder="your@email.com"
autocomplete="email"
required />
<span x-show="errors.email" x-text="errors.email"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<!-- Phone (Optional) -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Phone Number</span>
<input x-model="formData.phone"
:disabled="loading"
type="tel"
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"
placeholder="+352 123 456 789" />
</label>
<!-- Password -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Password <span class="text-red-600">*</span>
</span>
<div class="relative">
<input x-model="formData.password"
:disabled="loading"
@input="clearError('password')"
:type="showPassword ? 'text' : 'password'"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.password }"
placeholder="At least 8 characters"
autocomplete="new-password"
required />
<button type="button"
@click="showPassword = !showPassword"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span x-text="showPassword ? '👁️' : '👁️‍🗨️'"></span>
</button>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Must contain at least 8 characters, one letter, and one number
</p>
<span x-show="errors.password" x-text="errors.password"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<!-- Confirm Password -->
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Confirm Password <span class="text-red-600">*</span>
</span>
<input x-model="confirmPassword"
:disabled="loading"
@input="clearError('confirmPassword')"
:type="showPassword ? 'text' : 'password'"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
:class="{ 'border-red-600': errors.confirmPassword }"
placeholder="Re-enter your password"
autocomplete="new-password"
required />
<span x-show="errors.confirmPassword" x-text="errors.confirmPassword"
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
</label>
<!-- Marketing Consent -->
<div class="flex items-start mt-4">
<input type="checkbox"
x-model="formData.marketing_consent"
id="marketingConsent"
class="form-checkbox focus-primary focus:outline-none mt-1"
style="color: var(--color-primary);">
<label for="marketingConsent" class="ml-2 text-sm text-gray-600 dark:text-gray-400">
I'd like to receive news and special offers
</label>
</div>
<button type="submit" :disabled="loading"
class="btn-primary-theme block w-full px-4 py-2 mt-6 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Create Account</span>
<span x-show="loading" class="flex items-center justify-center">
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Creating account...
</span>
</button>
</form>
<hr class="my-8" />
<p class="mt-4 text-center">
<span class="text-sm text-gray-600 dark:text-gray-400">Already have an account?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
Sign in instead
</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Alert Box -->
<div x-show="alert.show"
x-transition
:class="'alert alert-' + alert.type"
x-text="alert.message"
></div>
<!-- Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
<!-- Registration Form -->
<form @submit.prevent="handleRegister">
<!-- First Name -->
<div class="form-group">
<label for="firstName">First Name <span class="required">*</span></label>
<input
type="text"
id="firstName"
x-model="formData.first_name"
required
placeholder="Enter your first name"
:class="{ 'error': errors.first_name }"
@input="clearError('first_name')"
>
<div x-show="errors.first_name"
x-text="errors.first_name"
class="error-message show"
></div>
</div>
<!-- Registration Logic -->
<script>
function customerRegistration() {
return {
// Data
formData: {
first_name: '',
last_name: '',
email: '',
phone: '',
password: '',
marketing_consent: false
},
confirmPassword: '',
showPassword: false,
loading: false,
errors: {},
alert: {
show: false,
type: 'error',
message: ''
},
dark: false,
<!-- Last Name -->
<div class="form-group">
<label for="lastName">Last Name <span class="required">*</span></label>
<input
type="text"
id="lastName"
x-model="formData.last_name"
required
placeholder="Enter your last name"
:class="{ 'error': errors.last_name }"
@input="clearError('last_name')"
>
<div x-show="errors.last_name"
x-text="errors.last_name"
class="error-message show"
></div>
</div>
// Initialize
init() {
// Check for dark mode preference
this.dark = localStorage.getItem('darkMode') === 'true';
},
<!-- Email -->
<div class="form-group">
<label for="email">Email Address <span class="required">*</span></label>
<input
type="email"
id="email"
x-model="formData.email"
required
placeholder="your@email.com"
:class="{ 'error': errors.email }"
@input="clearError('email')"
>
<div x-show="errors.email"
x-text="errors.email"
class="error-message show"
></div>
</div>
// Clear specific error
clearError(field) {
delete this.errors[field];
},
<!-- Phone (Optional) -->
<div class="form-group">
<label for="phone">Phone Number</label>
<input
type="tel"
id="phone"
x-model="formData.phone"
placeholder="+352 123 456 789"
>
</div>
// Clear all errors
clearAllErrors() {
this.errors = {};
this.alert.show = false;
},
<!-- Password -->
<div class="form-group">
<label for="password">Password <span class="required">*</span></label>
<div class="password-group">
<input
:type="showPassword ? 'text' : 'password'"
id="password"
x-model="formData.password"
required
placeholder="At least 8 characters"
:class="{ 'error': errors.password }"
@input="clearError('password')"
>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
>
<span x-text="showPassword ? '👁️' : '👁️‍🗨️'"></span>
</button>
</div>
<div class="form-help">
Must contain at least 8 characters, one letter, and one number
</div>
<div x-show="errors.password"
x-text="errors.password"
class="error-message show"
></div>
</div>
// Show alert
showAlert(message, type = 'error') {
this.alert = {
show: true,
type: type,
message: message
};
window.scrollTo({ top: 0, behavior: 'smooth' });
},
<!-- Confirm Password -->
<div class="form-group">
<label for="confirmPassword">Confirm Password <span class="required">*</span></label>
<input
:type="showPassword ? 'text' : 'password'"
id="confirmPassword"
x-model="confirmPassword"
required
placeholder="Re-enter your password"
:class="{ 'error': errors.confirmPassword }"
@input="clearError('confirmPassword')"
>
<div x-show="errors.confirmPassword"
x-text="errors.confirmPassword"
class="error-message show"
></div>
</div>
// Validate form
validateForm() {
this.clearAllErrors();
let isValid = true;
<!-- Marketing Consent -->
<div class="form-group">
<div class="remember-me">
<input
type="checkbox"
id="marketingConsent"
x-model="formData.marketing_consent"
>
<label for="marketingConsent" style="font-weight: normal;">
I'd like to receive news and special offers
</label>
</div>
</div>
// First name
if (!this.formData.first_name.trim()) {
this.errors.first_name = 'First name is required';
isValid = false;
}
<!-- Submit Button -->
<button
type="submit"
class="btn-login"
:disabled="loading"
>
<span x-show="loading" class="loading-spinner"></span>
<span x-text="loading ? 'Creating Account...' : 'Create Account'"></span>
</button>
</form>
// Last name
if (!this.formData.last_name.trim()) {
this.errors.last_name = 'Last name is required';
isValid = false;
}
<!-- Login Link -->
<div class="login-footer">
<div class="auth-footer-text">Already have an account?</div>
<a href="{{ base_url }}shop/account/login">Sign in instead</a>
</div>
</div>
// Email
if (!this.formData.email.trim()) {
this.errors.email = 'Email is required';
isValid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
this.errors.email = 'Please enter a valid email address';
isValid = false;
}
<script>
function customerRegistration() {
return {
// Data
formData: {
first_name: '',
last_name: '',
email: '',
phone: '',
password: '',
marketing_consent: false
},
confirmPassword: '',
showPassword: false,
loading: false,
errors: {},
alert: {
show: false,
type: 'error',
message: ''
},
// Password
if (!this.formData.password) {
this.errors.password = 'Password is required';
isValid = false;
} else if (this.formData.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
isValid = false;
} else if (!/[a-zA-Z]/.test(this.formData.password)) {
this.errors.password = 'Password must contain at least one letter';
isValid = false;
} else if (!/[0-9]/.test(this.formData.password)) {
this.errors.password = 'Password must contain at least one number';
isValid = false;
}
// Get vendor data from element
get vendorId() {
return this.$el.dataset.vendorId;
},
// Confirm password
if (this.formData.password !== this.confirmPassword) {
this.errors.confirmPassword = 'Passwords do not match';
isValid = false;
}
get vendorName() {
return this.$el.dataset.vendorName;
},
return isValid;
},
// Clear specific error
clearError(field) {
delete this.errors[field];
},
// Handle registration
async handleRegister() {
if (!this.validateForm()) {
return;
}
// Clear all errors
clearAllErrors() {
this.errors = {};
this.alert.show = false;
},
this.loading = true;
// Show alert
showAlert(message, type = 'error') {
this.alert = {
show: true,
type: type,
message: message
};
// Auto-hide success messages
if (type === 'success') {
setTimeout(() => {
this.alert.show = false;
}, 3000);
}
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
},
// Validate form
validateForm() {
this.clearAllErrors();
let isValid = true;
// First name
if (!this.formData.first_name.trim()) {
this.errors.first_name = 'First name is required';
isValid = false;
}
// Last name
if (!this.formData.last_name.trim()) {
this.errors.last_name = 'Last name is required';
isValid = false;
}
// Email
if (!this.formData.email.trim()) {
this.errors.email = 'Email is required';
isValid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
this.errors.email = 'Please enter a valid email address';
isValid = false;
}
// Password
if (!this.formData.password) {
this.errors.password = 'Password is required';
isValid = false;
} else if (this.formData.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
isValid = false;
} else if (!/[a-zA-Z]/.test(this.formData.password)) {
this.errors.password = 'Password must contain at least one letter';
isValid = false;
} else if (!/[0-9]/.test(this.formData.password)) {
this.errors.password = 'Password must contain at least one number';
isValid = false;
}
// Confirm password
if (this.formData.password !== this.confirmPassword) {
this.errors.confirmPassword = 'Passwords do not match';
isValid = false;
}
return isValid;
},
// Handle registration
async handleRegister() {
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
const response = await fetch(
`/api/v1/shop/auth/register`,
{
try {
const response = await fetch('/api/v1/shop/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.formData)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Registration failed');
}
);
const data = await response.json();
// Success!
this.showAlert('Account created successfully! Redirecting to login...', 'success');
if (!response.ok) {
throw new Error(data.detail || 'Registration failed');
// Redirect to login after 2 seconds
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login?registered=true';
}, 2000);
} catch (error) {
console.error('Registration error:', error);
this.showAlert(error.message || 'Registration failed. Please try again.');
} finally {
this.loading = false;
}
// Success!
this.showAlert(
'Account created successfully! Redirecting to login...',
'success'
);
// Redirect to login after 2 seconds
setTimeout(() => {
window.location.href = '/shop/account/login?registered=true';
}, 2000);
} catch (error) {
console.error('Registration error:', error);
this.showAlert(error.message || 'Registration failed. Please try again.');
} finally {
this.loading = false;
}
}
}
}
</script>
</script>
</body>
</html>