monacousa-portal/pages/signup.vue

647 lines
19 KiB
Vue

<template>
<div :class="containerClasses">
<v-container fluid class="fill-height">
<v-row class="fill-height" justify="center" align="center">
<v-col cols="12" sm="10" md="8" lg="6" xl="5">
<v-card :class="cardClasses" elevation="24" :loading="loading">
<v-card-text class="pa-8">
<!-- Logo and Welcome -->
<div class="text-center mb-6">
<v-img
src="/MONACOUSA-Flags_376x376.png"
width="80"
height="80"
class="mx-auto mb-3"
alt="MonacoUSA Logo"
/>
<h1 class="text-h4 font-weight-bold mb-2" style="color: #a31515;">
Join MonacoUSA
</h1>
<p class="text-body-1 text-medium-emphasis">
Register as a member of MonacoUSA Association
</p>
</div>
<!-- Registration Form -->
<v-form ref="form" v-model="valid" @submit.prevent="submitRegistration">
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model="firstName"
:key="'first-name-field'"
name="firstName"
autocomplete="given-name"
label="First Name"
:rules="nameRules"
prepend-inner-icon="mdi-account"
variant="outlined"
:disabled="loading"
required
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="lastName"
:key="'last-name-field'"
name="lastName"
autocomplete="family-name"
label="Last Name"
:rules="nameRules"
variant="outlined"
:disabled="loading"
required
/>
</v-col>
</v-row>
<v-text-field
v-model="email"
:key="'email-field'"
name="email"
autocomplete="email"
label="Email Address"
type="email"
:rules="emailRules"
prepend-inner-icon="mdi-email"
variant="outlined"
:disabled="loading"
required
/>
<PhoneInputWrapper
v-model="phone"
:key="'phone-field'"
label="Phone Number"
:rules="phoneRules"
:disabled="loading"
required
/>
<v-text-field
v-model="dateOfBirth"
:key="'dob-field'"
label="Date of Birth"
type="date"
:rules="dobRules"
prepend-inner-icon="mdi-calendar"
variant="outlined"
:disabled="loading"
required
/>
<v-textarea
v-model="address"
:key="'address-field'"
name="address"
autocomplete="street-address"
label="Address"
:rules="addressRules"
prepend-inner-icon="mdi-map-marker"
variant="outlined"
rows="3"
:disabled="loading"
required
/>
<MultipleNationalityInput
v-model="nationality"
:key="'nationality-field'"
label="Nationality"
:rules="nationalityRules"
:disabled="loading"
required
/>
<!-- Error Alert -->
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
class="mb-4"
closable
@click:close="errorMessage = ''"
>
<v-alert-title>Registration Failed</v-alert-title>
{{ errorMessage }}
</v-alert>
<v-btn
type="submit"
color="primary"
size="large"
block
:disabled="!valid || loading"
:loading="loading"
class="mb-4"
style="background-color: #a31515 !important; color: white !important;"
>
<v-icon left>mdi-account-plus</v-icon>
Register
</v-btn>
</v-form>
<!-- Payment Information -->
<v-divider class="my-6" />
<div class="payment-info">
<div class="d-flex align-center mb-4">
<v-icon color="#a31515" class="mr-2">mdi-bank</v-icon>
<h3 class="text-h6" style="color: #a31515;">Membership Dues Payment</h3>
</div>
<div class="payment-details" style="color: #000 !important;">
<p class="text-body-1 mb-3" style="color: #000 !important;">
After registration, please transfer your annual membership dues:
</p>
<v-row dense class="mb-2">
<v-col cols="4">
<span class="text-body-2 font-weight-bold" style="color: #000 !important;">Amount:</span>
</v-col>
<v-col cols="8">
<span class="text-body-2" style="color: #000 !important;">€{{ registrationConfig.membershipFee }}/year</span>
</v-col>
</v-row>
<v-row dense class="mb-2" v-if="registrationConfig.iban">
<v-col cols="4">
<span class="text-body-2 font-weight-bold" style="color: #000 !important;">IBAN:</span>
</v-col>
<v-col cols="8">
<span class="text-body-2 font-family-monospace" style="color: #000 !important;">{{ registrationConfig.iban }}</span>
</v-col>
</v-row>
<v-row dense class="mb-2" v-if="registrationConfig.accountHolder">
<v-col cols="4">
<span class="text-body-2 font-weight-bold" style="color: #000 !important;">Account:</span>
</v-col>
<v-col cols="8">
<span class="text-body-2" style="color: #000 !important;">{{ registrationConfig.accountHolder }}</span>
</v-col>
</v-row>
<v-divider class="my-3" />
<div class="text-body-2 d-flex align-center" style="color: #666 !important;">
<v-icon size="small" class="mr-2" color="#666">mdi-information</v-icon>
Your account will be activated once payment is verified by our administrators.
</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
<!-- Registration Success Dialog -->
<RegistrationSuccessDialog
v-model="showSuccessDialog"
:member-data="registrationResult"
:payment-info="registrationConfig"
@go-to-login="goToLogin"
/>
</div>
</template>
<script setup lang="ts">
import type { RegistrationFormData, RecaptchaConfig, RegistrationConfig } from '~/utils/types';
// Declare global grecaptcha interface for TypeScript
declare global {
interface Window {
grecaptcha: {
ready: (callback: () => void) => void;
execute: (siteKey: string, options: { action: string }) => Promise<string>;
};
}
}
// Page metadata
definePageMeta({
layout: false
});
// Configs with fallback defaults
const recaptchaConfig = ref<RecaptchaConfig>({ siteKey: '', secretKey: '' });
const registrationConfig = ref<RegistrationConfig>({
membershipFee: 150,
iban: 'MC58 1756 9000 0104 0050 1001 860',
accountHolder: 'ASSOCIATION MONACO USA'
});
// Reactive data - Using individual refs to prevent Vue reactivity corruption
const firstName = ref('');
const lastName = ref('');
const email = ref('');
const phone = ref('');
const dateOfBirth = ref('');
const address = ref('');
const nationality = ref('');
// Computed property to create form object for submission
const form = computed(() => ({
first_name: firstName.value,
last_name: lastName.value,
email: email.value,
phone: phone.value,
date_of_birth: dateOfBirth.value,
address: address.value,
nationality: nationality.value
}));
const valid = ref(false);
const loading = ref(false);
const recaptchaToken = ref('');
const successMessage = ref('');
const errorMessage = ref('');
const configsLoaded = ref(false);
// Success dialog state
const showSuccessDialog = ref(false);
const registrationResult = ref<{ memberId: string; email: string } | undefined>(undefined);
// Head configuration
useHead({
title: 'Register - MonacoUSA Portal',
meta: [
{ name: 'description', content: 'Register to become a member of MonacoUSA Association' },
{ name: 'robots', content: 'noindex, nofollow' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
]
});
// Static CSS classes based on device (no reactive dependencies)
const containerClasses = ref('signup-container');
const cardClasses = ref('signup-card');
// Form validation rules
const nameRules = [
(v: string) => !!v || 'Name is required',
(v: string) => v.length >= 2 || 'Name must be at least 2 characters'
];
const emailRules = [
(v: string) => !!v || 'Email is required',
(v: string) => /.+@.+\..+/.test(v) || 'Email must be valid'
];
const phoneRules = [
(v: string) => !!v || 'Phone number is required'
];
const dobRules = [
(v: string) => !!v || 'Date of birth is required',
(v: string) => {
if (!v) return true;
const birthDate = new Date(v);
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
let actualAge = age;
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
actualAge = age - 1;
}
return actualAge >= 18 || 'You must be at least 18 years old';
}
];
const addressRules = [
(v: string) => !!v || 'Address is required',
(v: string) => v.length >= 10 || 'Please provide a complete address'
];
const nationalityRules = [
(v: string) => !!v || 'Nationality is required'
];
// reCAPTCHA handling
function onRecaptchaVerified(token: string) {
recaptchaToken.value = token;
}
function onRecaptchaExpired() {
recaptchaToken.value = '';
}
// Template refs
const recaptcha = ref<any>(null);
// reCAPTCHA v3 token generation
async function generateRecaptchaToken(): Promise<string> {
if (!recaptchaConfig.value.siteKey || typeof window === 'undefined') {
return '';
}
return new Promise((resolve) => {
if (window.grecaptcha && window.grecaptcha.ready) {
window.grecaptcha.ready(() => {
window.grecaptcha.execute(recaptchaConfig.value.siteKey, { action: 'registration' }).then((token: string) => {
resolve(token);
}).catch(() => {
resolve('');
});
});
} else {
resolve('');
}
});
}
// Form submission
async function submitRegistration() {
if (!valid.value) {
return;
}
loading.value = true;
errorMessage.value = '';
successMessage.value = '';
try {
// Generate reCAPTCHA v3 token if configured
let token = '';
if (recaptchaConfig.value.siteKey) {
token = await generateRecaptchaToken();
}
const registrationData: RegistrationFormData = {
...form.value,
recaptcha_token: token
};
const response = await $fetch('/api/registration', {
method: 'POST',
body: registrationData
}) as any;
if (response?.success) {
// Set registration result data for dialog
registrationResult.value = {
memberId: response.data?.memberId || 'N/A',
email: form.value.email
};
// Show success dialog instead of just alert
showSuccessDialog.value = true;
// Reset form by resetting individual refs
firstName.value = '';
lastName.value = '';
email.value = '';
phone.value = '';
dateOfBirth.value = '';
address.value = '';
nationality.value = '';
}
} catch (error: any) {
console.error('Registration failed:', error);
errorMessage.value = error.data?.message || error.message || 'Registration failed. Please try again.';
} finally {
loading.value = false;
}
}
// Navigation methods
const goToLogin = () => {
navigateTo('/login');
};
// Load reCAPTCHA script dynamically
function loadRecaptchaScript(siteKey: string) {
if (typeof window === 'undefined' || document.querySelector(`script[src*="recaptcha/api.js"]`)) {
return; // Already loaded or not in browser
}
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
// Simplified initialization - prevent reload loops
onMounted(async () => {
// Prevent multiple initializations
if (typeof window === 'undefined') return;
// Static device detection from Nuxt Device Module - no reactive dependencies
const { isMobile, isIos, isSafari } = useDevice();
// Detect mobile Safari specifically
const isMobileSafari = isMobile && isIos && isSafari;
// Apply classes once (static, no reactivity)
const containerClassList = ['signup-container'];
if (isMobile) containerClassList.push('is-mobile');
if (isMobileSafari) containerClassList.push('is-mobile-safari');
if (isIos) containerClassList.push('is-ios');
containerClasses.value = containerClassList.join(' ');
const cardClassList = ['signup-card'];
if (isMobileSafari) {
cardClassList.push('performance-optimized');
cardClassList.push('no-backdrop-filter');
}
cardClasses.value = cardClassList.filter(Boolean).join(' ');
try {
// Load reCAPTCHA config
$fetch('/api/recaptcha-config')
.then((response: any) => {
if (response?.success && response?.data?.siteKey) {
recaptchaConfig.value.siteKey = response.data.siteKey;
loadRecaptchaScript(response.data.siteKey);
}
})
.catch(() => {
// Silently fail for reCAPTCHA - not critical
console.debug('reCAPTCHA config not available');
});
// Load registration config
$fetch('/api/registration-config')
.then((response: any) => {
if (response?.success) {
registrationConfig.value = response.data;
}
})
.catch(() => {
// Use defaults if config fails to load
console.debug('Using default registration config');
registrationConfig.value = {
membershipFee: 150,
iban: 'MC58 1756 9000 0104 0050 1001 860',
accountHolder: 'ASSOCIATION MONACO USA'
};
});
} catch (error) {
// Prevent any errors from bubbling up and causing reload
console.warn('Signup page initialization error:', error);
}
});
</script>
<style scoped>
/* Base container styles */
.signup-container {
min-height: 100vh;
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
background: linear-gradient(rgba(163, 21, 21, 0.7), rgba(0, 0, 0, 0.5)),
url('/monaco_high_res.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: scroll;
padding: 20px 0;
position: relative;
overflow-x: hidden; /* Prevent horizontal scroll on mobile */
}
/* Mobile Safari optimizations */
.signup-container.is-mobile-safari {
min-height: 100vh;
min-height: -webkit-fill-available;
background-attachment: scroll !important; /* Force scroll attachment */
}
.signup-container.performance-mode {
will-change: auto; /* Reduce repainting */
transform: translateZ(0); /* Force hardware acceleration but lighter */
}
/* Ensure background covers full content */
.signup-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(rgba(163, 21, 21, 0.7), rgba(0, 0, 0, 0.5)),
url('/monaco_high_res.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: scroll;
z-index: -1;
min-height: 100%;
}
/* Performance mode background - simpler for mobile */
.signup-container.performance-mode::before {
background: linear-gradient(rgba(163, 21, 21, 0.8), rgba(0, 0, 0, 0.6));
/* Remove background image on low-performance devices */
}
/* Default card styles */
.signup-card {
backdrop-filter: blur(15px);
background: rgba(255, 255, 255, 0.95) !important;
border-radius: 20px !important;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
transition: all 0.3s ease;
max-width: 600px;
width: 100%;
}
/* Performance optimized card */
.signup-card.performance-optimized {
backdrop-filter: none; /* Remove expensive filter */
background: rgba(255, 255, 255, 0.98) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important; /* Lighter shadow */
transition: none; /* Remove animations for better performance */
}
/* No backdrop filter fallback */
.signup-card.no-backdrop-filter {
backdrop-filter: none;
background: rgba(255, 255, 255, 0.98) !important;
}
.signup-card:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
}
/* Disable hover effects on performance mode */
.signup-card.performance-optimized:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important;
transform: none;
}
.payment-info {
background: rgba(163, 21, 21, 0.05);
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(163, 21, 21, 0.1);
}
.payment-details * {
color: #000 !important;
}
/* Custom scrollbar for mobile */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(163, 21, 21, 0.5);
border-radius: 2px;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.signup-container {
background-attachment: scroll;
padding: 16px;
}
.signup-card {
margin: 0;
}
}
/* Loading state styles */
.v-btn--loading {
pointer-events: none;
}
/* Form field focus styles */
.v-field--focused {
border-color: #a31515 !important;
}
.v-field--focused .v-field__outline {
border-color: #a31515 !important;
}
/* Ensure PhoneInputWrapper and MultipleNationalityInput align properly */
:deep(.v-field) {
width: 100%;
}
:deep(.v-input) {
width: 100%;
}
:deep(.v-text-field) {
width: 100%;
}
</style>