Replace date-fns with native date formatting and remove unused code
All checks were successful
Build And Push Image / docker (push) Successful in 1m34s
All checks were successful
Build And Push Image / docker (push) Successful in 1m34s
Remove date-fns dependency in favor of native Intl.DateTimeFormat APIs, clean up obsolete admin endpoints, utility files, and archived documentation. Consolidate docs structure and remove unused plugins.
This commit is contained in:
@@ -179,13 +179,26 @@ definePageMeta({
|
||||
middleware: 'guest'
|
||||
});
|
||||
|
||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
||||
// Device detection
|
||||
const isMobile = ref(false);
|
||||
const isMobileSafari = ref(false);
|
||||
|
||||
// Static device detection - no reactive dependencies
|
||||
const deviceInfo = getStaticDeviceInfo();
|
||||
// Initialize device detection on mount
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
const userAgent = navigator.userAgent;
|
||||
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||
}
|
||||
});
|
||||
|
||||
// Static CSS classes - computed once, never reactive
|
||||
const containerClasses = ref(getDeviceCssClasses('password-setup-page'));
|
||||
// CSS classes based on device detection
|
||||
const containerClasses = computed(() => {
|
||||
const classes = ['password-setup-page'];
|
||||
if (isMobile.value) classes.push('is-mobile');
|
||||
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-mode');
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
// Reactive state
|
||||
const loading = ref(false);
|
||||
@@ -265,7 +278,7 @@ useHead({
|
||||
name: 'description',
|
||||
content: 'Set your password to complete your MonacoUSA Portal registration.'
|
||||
},
|
||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover' }
|
||||
]
|
||||
});
|
||||
|
||||
@@ -333,24 +346,9 @@ const setupPassword = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Component initialization - Safari iOS reload loop prevention
|
||||
// Component initialization
|
||||
onMounted(async () => {
|
||||
console.log('[setup-password] Password setup page loaded for:', email.value);
|
||||
|
||||
// CRITICAL: Check reload loop prevention first
|
||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
||||
const canLoad = initReloadLoopPrevention('setup-password-page');
|
||||
|
||||
if (!canLoad) {
|
||||
console.error('[setup-password] Page load blocked by reload loop prevention system');
|
||||
return; // Stop all initialization if blocked
|
||||
}
|
||||
|
||||
// Apply mobile Safari optimizations early
|
||||
if (deviceInfo.isMobileSafari) {
|
||||
applyMobileSafariOptimizations();
|
||||
console.log('[setup-password] Mobile Safari optimizations applied');
|
||||
}
|
||||
|
||||
// Check if we have required parameters
|
||||
if (!email.value) {
|
||||
|
||||
@@ -91,26 +91,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
middleware: 'guest'
|
||||
});
|
||||
|
||||
// Get query parameters - static to prevent reload loops
|
||||
// Get query parameters
|
||||
const route = useRoute();
|
||||
const email = ref((route.query.email as string) || '');
|
||||
const partialWarning = ref(route.query.warning === 'partial');
|
||||
|
||||
// Static device detection - no reactive dependencies
|
||||
const deviceInfo = getStaticDeviceInfo();
|
||||
// Simple device detection
|
||||
const isMobile = ref(false);
|
||||
const isMobileSafari = ref(false);
|
||||
|
||||
// Static CSS classes - computed once, never reactive
|
||||
const containerClasses = ref(getDeviceCssClasses('verification-success'));
|
||||
// Initialize device detection on mount
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
const userAgent = navigator.userAgent;
|
||||
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||
}
|
||||
});
|
||||
|
||||
// Static setup password URL - no reactive dependencies
|
||||
const setupPasswordUrl = 'https://auth.monacousa.org/realms/monacousa/account/';
|
||||
// CSS classes based on device detection
|
||||
const containerClasses = computed(() => {
|
||||
const classes = ['verification-success'];
|
||||
if (isMobile.value) classes.push('is-mobile');
|
||||
if (isMobileSafari.value) classes.push('is-mobile-safari');
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
// Set page title with mobile viewport optimization
|
||||
useHead({
|
||||
@@ -120,7 +130,10 @@ useHead({
|
||||
name: 'description',
|
||||
content: 'Your email has been successfully verified. You can now log in to the MonacoUSA Portal.'
|
||||
},
|
||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -134,19 +147,12 @@ const goToPasswordSetup = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Track verification - Safari iOS reload loop prevention
|
||||
// Track verification
|
||||
onMounted(() => {
|
||||
console.log('[verify-success] Email verification completed', {
|
||||
email: email.value,
|
||||
partialWarning: partialWarning.value,
|
||||
setupPasswordUrl: setupPasswordUrl
|
||||
partialWarning: partialWarning.value
|
||||
});
|
||||
|
||||
// Apply mobile Safari optimizations early
|
||||
if (deviceInfo.isMobileSafari) {
|
||||
applyMobileSafariOptimizations();
|
||||
console.log('[verify-success] Mobile Safari optimizations applied');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -54,17 +54,14 @@
|
||||
Verifying Your Email
|
||||
</h1>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis" v-if="verificationState">
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
{{ statusMessage || 'Please wait while we verify your email address...' }}
|
||||
</p>
|
||||
<p class="text-body-1 text-medium-emphasis" v-else>
|
||||
Please wait while we verify your email address...
|
||||
</p>
|
||||
|
||||
<!-- Attempt Counter -->
|
||||
<div v-if="verificationState && verificationState.attempts > 1" class="mt-2">
|
||||
<div v-if="attemptCount > 1" class="mt-2">
|
||||
<v-chip size="small" color="primary" variant="outlined">
|
||||
Attempt {{ verificationState.attempts }}/{{ verificationState.maxAttempts }}
|
||||
Attempt {{ attemptCount }}/{{ maxAttempts }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +85,7 @@
|
||||
</p>
|
||||
|
||||
<!-- Circuit Breaker Status -->
|
||||
<div v-if="verificationState && statusMessage" class="mb-4">
|
||||
<div v-if="statusMessage" class="mb-4">
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
@@ -210,18 +207,6 @@ definePageMeta({
|
||||
middleware: 'guest'
|
||||
});
|
||||
|
||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
||||
import {
|
||||
getVerificationState,
|
||||
initVerificationState,
|
||||
recordAttempt,
|
||||
shouldBlockVerification,
|
||||
getStatusMessage,
|
||||
navigateWithFallback,
|
||||
getMobileNavigationDelay,
|
||||
type VerificationAttempt
|
||||
} from '~/utils/verification-state';
|
||||
|
||||
// Get route and token immediately
|
||||
const route = useRoute();
|
||||
const token = route.query.token as string || '';
|
||||
@@ -231,17 +216,33 @@ const verifying = ref(false);
|
||||
const error = ref('');
|
||||
const partialSuccess = ref(false);
|
||||
|
||||
// Verification state management
|
||||
const verificationState = ref<VerificationAttempt | null>(null);
|
||||
// Simple retry logic
|
||||
const isBlocked = ref(false);
|
||||
const canRetry = ref(true);
|
||||
const statusMessage = ref('');
|
||||
const attemptCount = ref(0);
|
||||
const maxAttempts = 3;
|
||||
|
||||
// Static device detection - no reactive dependencies
|
||||
const deviceInfo = getStaticDeviceInfo();
|
||||
// Device detection
|
||||
const isMobile = ref(false);
|
||||
const isMobileSafari = ref(false);
|
||||
|
||||
// Static container classes - must be reactive for template
|
||||
const containerClasses = ref(getDeviceCssClasses('verification-page'));
|
||||
// Initialize device detection on mount
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
const userAgent = navigator.userAgent;
|
||||
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||
}
|
||||
});
|
||||
|
||||
// CSS classes based on device detection
|
||||
const containerClasses = computed(() => {
|
||||
const classes = ['verification-page'];
|
||||
if (isMobile.value) classes.push('is-mobile');
|
||||
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-mode');
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
// Set page title with mobile viewport optimization
|
||||
useHead({
|
||||
@@ -251,49 +252,43 @@ useHead({
|
||||
name: 'description',
|
||||
content: 'Verifying your email address for the MonacoUSA Portal.'
|
||||
},
|
||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Update UI state based on verification state
|
||||
// Simple verification logic
|
||||
const updateUIState = () => {
|
||||
if (!verificationState.value) return;
|
||||
|
||||
statusMessage.value = getStatusMessage(verificationState.value);
|
||||
isBlocked.value = shouldBlockVerification(token);
|
||||
canRetry.value = verificationState.value.attempts < verificationState.value.maxAttempts && !isBlocked.value;
|
||||
|
||||
console.log('[auth/verify] UI State updated:', {
|
||||
status: verificationState.value.status,
|
||||
attempts: verificationState.value.attempts,
|
||||
isBlocked: isBlocked.value,
|
||||
canRetry: canRetry.value
|
||||
});
|
||||
if (attemptCount.value >= maxAttempts) {
|
||||
isBlocked.value = true;
|
||||
canRetry.value = false;
|
||||
statusMessage.value = `Too many failed attempts. Please wait before trying again.`;
|
||||
} else {
|
||||
canRetry.value = attemptCount.value < maxAttempts;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify email function with circuit breaker
|
||||
// Verify email function
|
||||
const verifyEmail = async () => {
|
||||
if (!token) {
|
||||
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize or get existing verification state
|
||||
verificationState.value = initVerificationState(token);
|
||||
updateUIState();
|
||||
|
||||
// Check if verification should be blocked
|
||||
if (shouldBlockVerification(token)) {
|
||||
console.log('[auth/verify] Verification blocked by circuit breaker');
|
||||
if (attemptCount.value >= maxAttempts) {
|
||||
isBlocked.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[auth/verify] Starting verification attempt ${verificationState.value.attempts + 1}/${verificationState.value.maxAttempts}`);
|
||||
|
||||
try {
|
||||
verifying.value = true;
|
||||
error.value = '';
|
||||
partialSuccess.value = false;
|
||||
attemptCount.value++;
|
||||
|
||||
console.log(`[auth/verify] Starting verification attempt ${attemptCount.value}/${maxAttempts}`);
|
||||
|
||||
// Call the API endpoint to verify the email
|
||||
const response = await $fetch(`/api/auth/verify-email?token=${token}`, {
|
||||
@@ -302,10 +297,6 @@ const verifyEmail = async () => {
|
||||
|
||||
console.log('[auth/verify] Email verification successful:', response);
|
||||
|
||||
// Record successful attempt
|
||||
verificationState.value = recordAttempt(token, true);
|
||||
updateUIState();
|
||||
|
||||
// Extract response data
|
||||
const email = response?.data?.email || '';
|
||||
const isPartialSuccess = response?.data?.partialSuccess || false;
|
||||
@@ -335,26 +326,22 @@ const verifyEmail = async () => {
|
||||
redirectUrl += '?' + queryParams.join('&');
|
||||
}
|
||||
|
||||
// Use progressive navigation with mobile delay
|
||||
const navigationDelay = getMobileNavigationDelay();
|
||||
console.log(`[auth/verify] Navigating to success page with ${navigationDelay}ms delay`);
|
||||
// Navigate to success page
|
||||
console.log(`[auth/verify] Navigating to success page`);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await navigateWithFallback(redirectUrl, { replace: true });
|
||||
await navigateTo(redirectUrl, { replace: true });
|
||||
} catch (navError) {
|
||||
console.error('[auth/verify] Navigation failed:', navError);
|
||||
// Final fallback - direct window location
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
}, navigationDelay);
|
||||
}, 500);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('[auth/verify] Email verification failed:', err);
|
||||
|
||||
// Record failed attempt
|
||||
const errorMessage = err.data?.message || err.message || 'Email verification failed';
|
||||
verificationState.value = recordAttempt(token, false, errorMessage);
|
||||
updateUIState();
|
||||
|
||||
// Set error message based on status code
|
||||
@@ -367,7 +354,7 @@ const verifyEmail = async () => {
|
||||
} else if (err.statusCode === 404) {
|
||||
error.value = 'User not found. The verification token may be invalid.';
|
||||
} else {
|
||||
error.value = errorMessage;
|
||||
error.value = err.data?.message || err.message || 'Email verification failed';
|
||||
}
|
||||
|
||||
verifying.value = false;
|
||||
@@ -385,40 +372,15 @@ const retryVerification = async () => {
|
||||
await verifyEmail();
|
||||
};
|
||||
|
||||
// Component initialization - Safari iOS reload loop prevention
|
||||
// Component initialization
|
||||
onMounted(async () => {
|
||||
console.log('[auth/verify] Component mounted with token:', token?.substring(0, 20) + '...');
|
||||
|
||||
// CRITICAL: Check reload loop prevention first
|
||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
||||
const canLoad = initReloadLoopPrevention('verify-page');
|
||||
|
||||
if (!canLoad) {
|
||||
console.error('[auth/verify] Page load blocked by reload loop prevention system');
|
||||
return; // Stop all initialization if blocked
|
||||
}
|
||||
|
||||
// Apply mobile Safari optimizations early
|
||||
if (deviceInfo.isMobileSafari) {
|
||||
applyMobileSafariOptimizations();
|
||||
console.log('[auth/verify] Mobile Safari optimizations applied');
|
||||
}
|
||||
|
||||
// Check if token exists
|
||||
if (!token) {
|
||||
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize verification state
|
||||
verificationState.value = initVerificationState(token, 3);
|
||||
updateUIState();
|
||||
|
||||
// Check if verification is blocked before starting
|
||||
if (shouldBlockVerification(token)) {
|
||||
console.log('[auth/verify] Verification blocked by circuit breaker on mount');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start verification process with a small delay to ensure stability
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -507,28 +507,64 @@ const deleteMember = async () => {
|
||||
};
|
||||
|
||||
const handleMemberCreated = (newMember: Member) => {
|
||||
console.log('[member-list] =====================================');
|
||||
console.log('[member-list] handleMemberCreated called with:', JSON.stringify(newMember, null, 2));
|
||||
console.log('[member-list] newMember fields:', Object.keys(newMember));
|
||||
console.log('[member-list] FullName value:', `"${newMember.FullName}"`);
|
||||
console.log('[member-list] first_name value:', `"${newMember.first_name}"`);
|
||||
console.log('[member-list] last_name value:', `"${newMember.last_name}"`);
|
||||
console.log('[member-list] nationality value:', `"${newMember.nationality}"`);
|
||||
console.log('[member-list] email value:', `"${newMember.email}"`);
|
||||
console.log('[member-list] member_id value:', `"${newMember.member_id}"`);
|
||||
console.log('[member-list] membership_status value:', `"${newMember.membership_status}"`);
|
||||
|
||||
// Calculate FullName if it doesn't exist
|
||||
// ADVANCED DEBUGGING: Check if data is actually missing
|
||||
const hasFirstName = !!(newMember.first_name && newMember.first_name.trim());
|
||||
const hasLastName = !!(newMember.last_name && newMember.last_name.trim());
|
||||
const hasFullName = !!(newMember.FullName && newMember.FullName.trim());
|
||||
|
||||
console.log('[member-list] Data validation:');
|
||||
console.log(' - hasFirstName:', hasFirstName);
|
||||
console.log(' - hasLastName:', hasLastName);
|
||||
console.log(' - hasFullName:', hasFullName);
|
||||
|
||||
// If the API response is missing data, refresh the entire member list instead
|
||||
if (!hasFirstName || !hasLastName || !hasFullName) {
|
||||
console.error('[member-list] ❌ API response missing critical member data, refreshing member list...');
|
||||
loadMembers();
|
||||
showSuccess.value = true;
|
||||
successMessage.value = 'Member created successfully. Refreshing member list...';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate FullName with robust fallback
|
||||
const fullName = newMember.FullName ||
|
||||
`${newMember.first_name || ''} ${newMember.last_name || ''}`.trim() ||
|
||||
'New Member';
|
||||
|
||||
console.log('[member-list] Calculated FullName:', `"${fullName}"`);
|
||||
|
||||
// Ensure the member has a FullName for display
|
||||
const memberWithFullName = {
|
||||
// Ensure the member has complete data for display
|
||||
const memberWithCompleteData = {
|
||||
...newMember,
|
||||
FullName: fullName
|
||||
FullName: fullName,
|
||||
// Ensure all required fields are present
|
||||
first_name: newMember.first_name || '',
|
||||
last_name: newMember.last_name || '',
|
||||
nationality: newMember.nationality || '',
|
||||
email: newMember.email || '',
|
||||
membership_status: newMember.membership_status || 'Active'
|
||||
};
|
||||
|
||||
members.value.unshift(memberWithFullName);
|
||||
console.log('[member-list] Final member data:', JSON.stringify(memberWithCompleteData, null, 2));
|
||||
console.log('[member-list] Adding member to beginning of list...');
|
||||
|
||||
members.value.unshift(memberWithCompleteData);
|
||||
showSuccess.value = true;
|
||||
successMessage.value = `${fullName} has been added successfully.`;
|
||||
|
||||
console.log('[member-list] ✅ Member added to local list, total count:', members.value.length);
|
||||
console.log('[member-list] =====================================');
|
||||
};
|
||||
|
||||
const handleMemberUpdated = (updatedMember: Member) => {
|
||||
|
||||
@@ -68,6 +68,70 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Profile Photo -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12">
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||
<v-icon class="mr-2" color="primary">mdi-account-circle</v-icon>
|
||||
Profile Photo
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center flex-wrap">
|
||||
<!-- Avatar Preview -->
|
||||
<div class="mr-6 mb-4 text-center">
|
||||
<ProfileAvatar
|
||||
v-if="memberData"
|
||||
:member-id="memberData.member_id"
|
||||
:member-name="fullName"
|
||||
:first-name="memberData.first_name"
|
||||
:last-name="memberData.last_name"
|
||||
size="large"
|
||||
:key="avatarBustKey"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis">Current Photo</p>
|
||||
</div>
|
||||
|
||||
<!-- Upload Controls -->
|
||||
<div class="flex-grow-1 mb-4">
|
||||
<v-file-input
|
||||
v-model="selectedFiles"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
label="Choose new profile photo (uploads automatically)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-icon="mdi-camera"
|
||||
show-size
|
||||
:disabled="uploading || deleting"
|
||||
:loading="uploading"
|
||||
@update:model-value="onSelectImage"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-delete"
|
||||
:loading="deleting"
|
||||
:disabled="uploading || !memberData?.member_id"
|
||||
@click="confirmDelete = true"
|
||||
>
|
||||
Remove Photo
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">
|
||||
Supported formats: JPG, PNG, WEBP • Maximum size: 5MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Profile Information -->
|
||||
<v-row>
|
||||
<!-- Personal Information -->
|
||||
@@ -295,6 +359,35 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<v-dialog v-model="confirmDelete" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">
|
||||
Remove Profile Photo?
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to remove your profile photo? This action cannot be undone.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="confirmDelete = false"
|
||||
:disabled="deleting"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:loading="deleting"
|
||||
@click="confirmDeleteImage"
|
||||
>
|
||||
Remove Photo
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -310,13 +403,18 @@ const { user, userTier } = useAuth();
|
||||
|
||||
// Reactive state
|
||||
const loading = ref(true);
|
||||
const memberData = ref<Member | null>(null);
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
});
|
||||
|
||||
// Fetch complete member data (same as user.vue)
|
||||
const { data: sessionData, pending: sessionPending, error: sessionError, refresh: refreshSession } =
|
||||
await useFetch<{ success: boolean; member: Member }>('/api/auth/session', { server: false });
|
||||
|
||||
const memberData = computed<Member | null>(() => sessionData.value?.member || null);
|
||||
|
||||
// Computed properties
|
||||
const fullName = computed(() => {
|
||||
if (memberData.value) {
|
||||
@@ -336,19 +434,20 @@ const daysRemaining = computed(() => {
|
||||
return diffDays;
|
||||
});
|
||||
|
||||
// Profile image state
|
||||
const uploading = ref(false);
|
||||
const deleting = ref(false);
|
||||
const avatarBustKey = ref(0);
|
||||
const selectedFiles = ref<File[]>([]);
|
||||
const confirmDelete = ref(false);
|
||||
|
||||
// Methods
|
||||
const loadMemberData = async () => {
|
||||
if (!user.value?.email) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await $fetch('/api/members') as any;
|
||||
const members = response?.data || response?.list || [];
|
||||
|
||||
// Find member by email
|
||||
const member = members.find((m: any) => m.email === user.value?.email);
|
||||
if (member) {
|
||||
memberData.value = member;
|
||||
await refreshSession();
|
||||
if (!sessionData.value?.member) {
|
||||
throw new Error('Missing member in session');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load member data:', error);
|
||||
@@ -362,6 +461,65 @@ const loadMemberData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Profile image helpers
|
||||
const onSelectImage = async (files: File[] | File | null) => {
|
||||
const fileList = Array.isArray(files) ? files : files ? [files] : [];
|
||||
if (fileList.length === 0) return;
|
||||
const file = fileList[0];
|
||||
|
||||
// Basic validation
|
||||
const maxBytes = 5 * 1024 * 1024; // 5MB
|
||||
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
if (!allowed.includes(file.type)) {
|
||||
snackbar.value = { show: true, message: 'Only JPG, PNG or WEBP images are allowed.', color: 'error' };
|
||||
return;
|
||||
}
|
||||
if (file.size > maxBytes) {
|
||||
snackbar.value = { show: true, message: 'Image must be 5MB or smaller.', color: 'error' };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uploading.value = true;
|
||||
const body = new FormData();
|
||||
body.append('file', file);
|
||||
await $fetch('/api/profile/upload-image', {
|
||||
method: 'POST',
|
||||
body
|
||||
});
|
||||
avatarBustKey.value++;
|
||||
selectedFiles.value = []; // Clear the file input
|
||||
snackbar.value = { show: true, message: 'Profile image updated.', color: 'success' };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snackbar.value = { show: true, message: 'Failed to upload image.', color: 'error' };
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteImage = async () => {
|
||||
confirmDelete.value = false;
|
||||
await onDeleteImage();
|
||||
};
|
||||
|
||||
const onDeleteImage = async () => {
|
||||
if (!memberData.value?.member_id) return;
|
||||
try {
|
||||
deleting.value = true;
|
||||
await $fetch(`/api/profile/image/${encodeURIComponent(memberData.value.member_id)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
avatarBustKey.value++;
|
||||
snackbar.value = { show: true, message: 'Profile image removed.', color: 'success' };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snackbar.value = { show: true, message: 'Failed to delete image.', color: 'error' };
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const copyMemberID = async () => {
|
||||
if (!memberData.value?.member_id) return;
|
||||
|
||||
@@ -431,6 +589,11 @@ onMounted(() => {
|
||||
loadMemberData();
|
||||
});
|
||||
|
||||
// Watch for session loading
|
||||
watch(sessionPending, (isPending) => {
|
||||
loading.value = isPending;
|
||||
});
|
||||
|
||||
// Watch for user changes
|
||||
watch(user, () => {
|
||||
if (user.value) {
|
||||
|
||||
@@ -211,7 +211,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RegistrationFormData } from '~/utils/types';
|
||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
||||
import { loadAllConfigs } from '~/utils/config-cache';
|
||||
|
||||
// Page metadata
|
||||
@@ -219,28 +218,47 @@ definePageMeta({
|
||||
layout: false
|
||||
});
|
||||
|
||||
// Static device detection - no reactive dependencies
|
||||
const deviceInfo = getStaticDeviceInfo();
|
||||
// Device detection
|
||||
const isMobile = ref(false);
|
||||
const isMobileSafari = ref(false);
|
||||
|
||||
// Head configuration with static device-optimized viewport
|
||||
// Initialize device detection on mount
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
const userAgent = navigator.userAgent;
|
||||
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||
}
|
||||
});
|
||||
|
||||
// Head configuration with mobile-optimized viewport
|
||||
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: getMobileSafariViewportMeta() }
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Static CSS classes - computed once, never reactive
|
||||
const containerClasses = ref(getDeviceCssClasses('signup-container'));
|
||||
const cardClasses = ref((() => {
|
||||
// CSS classes based on device detection
|
||||
const containerClasses = computed(() => {
|
||||
const classes = ['signup-container'];
|
||||
if (isMobile.value) classes.push('is-mobile');
|
||||
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-optimized');
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
const cardClasses = computed(() => {
|
||||
const classes = ['signup-card'];
|
||||
if (deviceInfo.isMobileSafari) {
|
||||
if (isMobileSafari.value) {
|
||||
classes.push('performance-optimized', 'no-backdrop-filter');
|
||||
}
|
||||
return classes.join(' ');
|
||||
})()); // Execute immediately and store the result, not the function
|
||||
});
|
||||
|
||||
// Form data - individual refs to prevent Vue reactivity corruption
|
||||
const firstName = ref('');
|
||||
@@ -426,30 +444,13 @@ const goToLogin = () => {
|
||||
// Flag to prevent multiple initialization calls
|
||||
let initialized = false;
|
||||
|
||||
// Component initialization - Safari iOS reload loop prevention
|
||||
// Component initialization
|
||||
onMounted(async () => {
|
||||
// Prevent multiple initializations
|
||||
if (initialized || typeof window === 'undefined') return;
|
||||
|
||||
console.log('[signup] Starting signup page initialization...');
|
||||
|
||||
// CRITICAL: Check reload loop prevention first
|
||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
||||
const canLoad = initReloadLoopPrevention('signup-page');
|
||||
|
||||
if (!canLoad) {
|
||||
console.error('[signup] Page load blocked by reload loop prevention system');
|
||||
return; // Stop all initialization if blocked
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
|
||||
// Apply mobile Safari optimizations early
|
||||
if (deviceInfo.isMobileSafari) {
|
||||
applyMobileSafariOptimizations();
|
||||
console.log('[signup] Mobile Safari optimizations applied');
|
||||
}
|
||||
|
||||
// Set up reCAPTCHA callbacks before loading configs
|
||||
setupRecaptchaCallbacks();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user