Files
orion/app/templates/storefront/account/register.html
Samir Boulahtit 7245f79f7b refactor: rename shop to storefront for consistency
Rename all "shop" directories and references to "storefront" to match
the API and route naming convention already in use.

Renamed directories:
- app/templates/shop/ → app/templates/storefront/
- static/shop/ → static/storefront/
- app/templates/shared/macros/shop/ → .../macros/storefront/
- docs/frontend/shop/ → docs/frontend/storefront/

Renamed files:
- shop.css → storefront.css
- shop-layout.js → storefront-layout.js

Updated references in:
- app/routes/storefront_pages.py (21 template references)
- app/modules/cms/routes/pages/vendor.py
- app/templates/storefront/base.html (static paths)
- All storefront templates (extends/includes)
- docs/architecture/frontend-structure.md

This aligns the template/static naming with:
- Route file: storefront_pages.py
- API directory: app/api/v1/storefront/
- Module routes: */routes/api/storefront.py
- URL paths: /storefront/*

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:58:28 +01:00

378 lines
19 KiB
HTML

{# app/templates/storefront/account/register.html #}
{# standalone #}
<!DOCTYPE html>
<html :class="{ 'dark': dark }" x-data="customerRegistration()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create Account - {{ vendor.name }}</title>
<!-- Fonts: Local fallback + Google Fonts -->
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
{# 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 }}{# sanitized: admin-controlled #}
{% 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 v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.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">
<span class="inline w-4 h-4 mr-2" x-html="$icon('spinner', 'w-4 h-4 animate-spin')"></span>
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>
<!-- Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
<!-- 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,
// Initialize
init() {
// Check for dark mode preference
this.dark = localStorage.getItem('darkMode') === 'true';
},
// Clear specific error
clearError(field) {
delete this.errors[field];
},
// Clear all errors
clearAllErrors() {
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' });
},
// 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', {
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');
}
// Success!
this.showAlert('Account created successfully! Redirecting to login...', 'success');
// 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;
}
}
}
}
</script>
</body>
</html>