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
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@