From 503d68cd2dcd62c9817b68c199e749053383f314 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 14 Aug 2025 15:08:40 +0200 Subject: [PATCH] Replace date-fns with native date formatting and remove unused code 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. --- components/EventDetailsDialog.vue | 37 +- components/MultipleNationalityInput.vue | 31 +- components/PhoneInputWrapper.vue | 24 +- components/UpcomingEventBanner.vue | 45 +- docs-archive/DEPLOYMENT_FORCE_UPDATE.md | 20 - ...MAIL_VERIFICATION_RELOAD_LOOP_FIX_FINAL.md | 267 - ...ENTS_SYSTEM_BUGS_COMPREHENSIVE_ANALYSIS.md | 148 - docs-archive/INTEGRATION_REVIEW.md | 280 - ...MOBILE_BROWSER_RELOAD_LOOP_FIX_COMPLETE.md | 144 - ...LE_RELOAD_LOOP_PREVENTION_COMPREHENSIVE.md | 344 - .../MOBILE_SAFARI_KEYCLOAK_FIXES_SUMMARY.md | 325 - .../MOBILE_SAFARI_RELOAD_LOOP_FINAL_FIX.md | 190 - ...E_SAFARI_RELOAD_LOOP_FIX_IMPLEMENTATION.md | 274 - docs-archive/PORTAL_FIXES_SUMMARY.md | 142 - docs-archive/PWA_DISABLE_TEST_SUMMARY.md | 118 - .../SAFARI_RELOAD_LOOP_FIX_COMPLETE.md | 190 - docs/keycloak_api.json | 18768 ++++++++++++++++ .../minio_example_guide.md | 0 nuxt.config.ts | 58 +- package-lock.json | 1121 +- package.json | 7 +- pages/auth/setup-password.vue | 42 +- pages/auth/verify-success.vue | 44 +- pages/auth/verify.vue | 138 +- pages/dashboard/member-list.vue | 46 +- pages/dashboard/profile.vue | 183 +- pages/signup.vue | 57 +- plugins/config-cache-init.client.ts | 159 +- plugins/vuetify-tiptap.client.ts | 12 - server/api/admin/backfill-event-ids.post.ts | 104 - server/api/admin/cleanup-accounts.post.ts | 163 - server/api/admin/debug-rsvp-config.get.ts | 64 - .../admin/fix-event-attendee-counts.post.ts | 96 - .../api/admin/membership-status-fix.post.ts | 206 - server/api/admin/nocodb-test.post.ts | 135 - utils/mobile-utils.ts | 214 - utils/reload-loop-prevention.ts | 310 - utils/static-device-detection.ts | 128 - utils/verification-state.ts | 300 - utils/viewport-manager.ts | 142 - 40 files changed, 19225 insertions(+), 5851 deletions(-) delete mode 100644 docs-archive/DEPLOYMENT_FORCE_UPDATE.md delete mode 100644 docs-archive/EMAIL_VERIFICATION_RELOAD_LOOP_FIX_FINAL.md delete mode 100644 docs-archive/EVENTS_SYSTEM_BUGS_COMPREHENSIVE_ANALYSIS.md delete mode 100644 docs-archive/INTEGRATION_REVIEW.md delete mode 100644 docs-archive/MOBILE_BROWSER_RELOAD_LOOP_FIX_COMPLETE.md delete mode 100644 docs-archive/MOBILE_RELOAD_LOOP_PREVENTION_COMPREHENSIVE.md delete mode 100644 docs-archive/MOBILE_SAFARI_KEYCLOAK_FIXES_SUMMARY.md delete mode 100644 docs-archive/MOBILE_SAFARI_RELOAD_LOOP_FINAL_FIX.md delete mode 100644 docs-archive/MOBILE_SAFARI_RELOAD_LOOP_FIX_IMPLEMENTATION.md delete mode 100644 docs-archive/PORTAL_FIXES_SUMMARY.md delete mode 100644 docs-archive/PWA_DISABLE_TEST_SUMMARY.md delete mode 100644 docs-archive/SAFARI_RELOAD_LOOP_FIX_COMPLETE.md create mode 100644 docs/keycloak_api.json rename docs-archive/MinIO_Implementation_Examples_Guide => docs/minio_example_guide.md (100%) delete mode 100644 plugins/vuetify-tiptap.client.ts delete mode 100644 server/api/admin/backfill-event-ids.post.ts delete mode 100644 server/api/admin/cleanup-accounts.post.ts delete mode 100644 server/api/admin/debug-rsvp-config.get.ts delete mode 100644 server/api/admin/fix-event-attendee-counts.post.ts delete mode 100644 server/api/admin/membership-status-fix.post.ts delete mode 100644 server/api/admin/nocodb-test.post.ts delete mode 100644 utils/mobile-utils.ts delete mode 100644 utils/reload-loop-prevention.ts delete mode 100644 utils/static-device-detection.ts delete mode 100644 utils/verification-state.ts delete mode 100644 utils/viewport-manager.ts diff --git a/components/EventDetailsDialog.vue b/components/EventDetailsDialog.vue index 41ad31a..5008a8c 100644 --- a/components/EventDetailsDialog.vue +++ b/components/EventDetailsDialog.vue @@ -334,7 +334,36 @@ import type { Event, EventRSVP } from '~/utils/types'; import { useEvents } from '~/composables/useEvents'; import { useAuth } from '~/composables/useAuth'; -import { format } from 'date-fns'; +// Helper function to replace date-fns format +const formatDate = (date: Date, formatStr: string): string => { + if (formatStr === 'EEEE, MMMM d, yyyy') { + return date.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } else if (formatStr === 'MMM d') { + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }); + } else if (formatStr === 'MMM d, yyyy') { + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + } else if (formatStr === 'HH:mm') { + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + } + + return date.toLocaleDateString(); +}; interface Props { modelValue: boolean; @@ -431,9 +460,9 @@ const formatEventDate = computed(() => { const endDate = new Date(props.event.end_datetime); if (startDate.toDateString() === endDate.toDateString()) { - return format(startDate, 'EEEE, MMMM d, yyyy'); + return formatDate(startDate, 'EEEE, MMMM d, yyyy'); } else { - return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d, yyyy')}`; + return `${formatDate(startDate, 'MMM d')} - ${formatDate(endDate, 'MMM d, yyyy')}`; } }); @@ -442,7 +471,7 @@ const formatEventTime = computed(() => { const startDate = new Date(props.event.start_datetime); const endDate = new Date(props.event.end_datetime); - return `${format(startDate, 'HH:mm')} - ${format(endDate, 'HH:mm')}`; + return `${formatDate(startDate, 'HH:mm')} - ${formatDate(endDate, 'HH:mm')}`; }); const capacityPercentage = computed(() => { diff --git a/components/MultipleNationalityInput.vue b/components/MultipleNationalityInput.vue index a286899..26ca3bd 100644 --- a/components/MultipleNationalityInput.vue +++ b/components/MultipleNationalityInput.vue @@ -215,7 +215,18 @@ diff --git a/components/UpcomingEventBanner.vue b/components/UpcomingEventBanner.vue index e916645..b771ea3 100644 --- a/components/UpcomingEventBanner.vue +++ b/components/UpcomingEventBanner.vue @@ -148,7 +148,42 @@ diff --git a/pages/auth/verify.vue b/pages/auth/verify.vue index dc80026..081b13c 100644 --- a/pages/auth/verify.vue +++ b/pages/auth/verify.vue @@ -54,17 +54,14 @@ Verifying Your Email -

+

{{ statusMessage || 'Please wait while we verify your email address...' }}

-

- Please wait while we verify your email address... -

-
+
- Attempt {{ verificationState.attempts }}/{{ verificationState.maxAttempts }} + Attempt {{ attemptCount }}/{{ maxAttempts }}
@@ -88,7 +85,7 @@

-
+
(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(() => { diff --git a/pages/dashboard/member-list.vue b/pages/dashboard/member-list.vue index 6050676..8d90256 100644 --- a/pages/dashboard/member-list.vue +++ b/pages/dashboard/member-list.vue @@ -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) => { diff --git a/pages/dashboard/profile.vue b/pages/dashboard/profile.vue index d64680b..44fd9fa 100644 --- a/pages/dashboard/profile.vue +++ b/pages/dashboard/profile.vue @@ -68,6 +68,70 @@ + + + + + + mdi-account-circle + Profile Photo + + +
+ +
+ +

Current Photo

+
+ + +
+ + +
+ + Remove Photo + +
+ +

+ Supported formats: JPG, PNG, WEBP • Maximum size: 5MB +

+
+
+
+
+
+
+ @@ -295,6 +359,35 @@ + + + + + + Remove Profile Photo? + + + Are you sure you want to remove your profile photo? This action cannot be undone. + + + + + Cancel + + + Remove Photo + + + + @@ -310,13 +403,18 @@ const { user, userTier } = useAuth(); // Reactive state const loading = ref(true); -const memberData = ref(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(() => 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([]); +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) { diff --git a/pages/signup.vue b/pages/signup.vue index f1b774d..89bfff6 100644 --- a/pages/signup.vue +++ b/pages/signup.vue @@ -211,7 +211,6 @@