Replace date-fns with native date formatting and remove unused code
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:
2025-08-14 15:08:40 +02:00
parent 676bbc04f6
commit 503d68cd2d
40 changed files with 19225 additions and 5851 deletions

View File

@@ -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) => {

View File

@@ -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) {