Add global branding and implement member ID system
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
- Add MonacoUSA logo component with global header placement - Implement member ID generation and migration system - Create profile page and improve dashboard navigation - Add member ID as payment reference in dues banner - Enable support contact functionality with pre-filled email
This commit is contained in:
477
pages/dashboard/profile.vue
Normal file
477
pages/dashboard/profile.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<!-- Page Header -->
|
||||
<v-row class="mb-6">
|
||||
<v-col>
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.back()"
|
||||
class="mr-3"
|
||||
/>
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
|
||||
My Profile
|
||||
</h1>
|
||||
<p class="text-h6 text-medium-emphasis">
|
||||
View and manage your membership information
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Loading State -->
|
||||
<v-row v-if="loading" class="justify-center">
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
/>
|
||||
<p class="mt-4">Loading your profile...</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Profile Content -->
|
||||
<div v-else>
|
||||
<!-- Member ID Card -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12">
|
||||
<v-card elevation="2" class="member-id-card">
|
||||
<v-card-text class="pa-6">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="48" color="primary" class="mr-4">mdi-badge-account</v-icon>
|
||||
<div>
|
||||
<h2 class="text-h4 font-weight-bold" style="color: #a31515;">
|
||||
{{ memberData?.member_id || 'Member ID Pending' }}
|
||||
</h2>
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
Your unique MonacoUSA member identifier
|
||||
</p>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="memberData?.member_id"
|
||||
icon="mdi-content-copy"
|
||||
variant="outlined"
|
||||
@click="copyMemberID"
|
||||
:title="`Copy member ID: ${memberData.member_id}`"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Profile Information -->
|
||||
<v-row>
|
||||
<!-- Personal Information -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-card elevation="2" class="h-100">
|
||||
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||
<v-icon class="mr-2" color="primary">mdi-account-details</v-icon>
|
||||
Personal Information
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Full Name</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ fullName || 'Not provided' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Email Address</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ memberData?.email || user?.email || 'Not provided' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Phone Number</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ memberData?.phone || 'Not provided' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Date of Birth</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(memberData?.date_of_birth) || 'Not provided' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Nationality</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<div v-if="memberData?.nationality">
|
||||
<CountryFlag
|
||||
v-for="country in parseNationalities(memberData.nationality)"
|
||||
:key="country"
|
||||
:country="country"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>Not provided</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Address</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ memberData?.address || 'Not provided' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Membership Details -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-card elevation="2" class="h-100">
|
||||
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||
<v-icon class="mr-2" color="primary">mdi-card-membership</v-icon>
|
||||
Membership Details
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Member Since</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(memberData?.member_since) || 'Not provided' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Membership Status</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="getStatusColor(memberData?.membership_status)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ memberData?.membership_status || 'Pending' }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Account Tier</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="getTierColor(userTier)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ userTier?.toUpperCase() || 'USER' }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">Registration Date</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(memberData?.registration_date) || 'Not available' }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Dues and Payment Information -->
|
||||
<v-row class="mt-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-credit-card-outline</v-icon>
|
||||
Dues and Payment Information
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<div class="text-center">
|
||||
<v-icon
|
||||
:color="memberData?.current_year_dues_paid === 'true' ? 'success' : 'warning'"
|
||||
size="48"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ memberData?.current_year_dues_paid === 'true' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||
</v-icon>
|
||||
<h4 class="font-weight-bold">Current Year Dues</h4>
|
||||
<v-chip
|
||||
:color="memberData?.current_year_dues_paid === 'true' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ memberData?.current_year_dues_paid === 'true' ? 'PAID' : 'UNPAID' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<div>
|
||||
<h4 class="font-weight-bold mb-2">Last Payment Date</h4>
|
||||
<p>{{ formatDate(memberData?.membership_date_paid) || 'No payment recorded' }}</p>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<div>
|
||||
<h4 class="font-weight-bold mb-2">Payment Due Date</h4>
|
||||
<p>{{ formatDate(memberData?.payment_due_date) || 'Not set' }}</p>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<div>
|
||||
<h4 class="font-weight-bold mb-2">Days Remaining</h4>
|
||||
<p :class="getDaysRemainingColor(daysRemaining)">
|
||||
{{ daysRemaining >= 0 ? `${daysRemaining} days` : `${Math.abs(daysRemaining)} days overdue` }}
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Payment Reference Info -->
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div v-if="memberData?.member_id">
|
||||
<h4 class="font-weight-bold mb-2">Payment Reference for Wire Transfers</h4>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field
|
||||
:value="memberData.member_id"
|
||||
readonly
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
style="max-width: 200px;"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
variant="outlined"
|
||||
@click="copyMemberID"
|
||||
title="Copy member ID"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">
|
||||
Include this member ID in your wire transfer reference for payment identification.
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- Snackbar for notifications -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="3000"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
<template #actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="snackbar.show = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'dashboard',
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
const { user, userTier } = useAuth();
|
||||
|
||||
// Reactive state
|
||||
const loading = ref(true);
|
||||
const memberData = ref<Member | null>(null);
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
const fullName = computed(() => {
|
||||
if (memberData.value) {
|
||||
return `${memberData.value.first_name || ''} ${memberData.value.last_name || ''}`.trim();
|
||||
}
|
||||
return user.value?.name || '';
|
||||
});
|
||||
|
||||
const daysRemaining = computed(() => {
|
||||
if (!memberData.value?.payment_due_date) return 0;
|
||||
|
||||
const dueDate = new Date(memberData.value.payment_due_date);
|
||||
const today = new Date();
|
||||
const diffTime = dueDate.getTime() - today.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load member data:', error);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: 'Failed to load profile data. Please try refreshing the page.',
|
||||
color: 'error'
|
||||
};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const copyMemberID = async () => {
|
||||
if (!memberData.value?.member_id) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(memberData.value.member_id);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: `Member ID ${memberData.value.member_id} copied to clipboard!`,
|
||||
color: 'success'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard:', error);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: 'Failed to copy member ID to clipboard.',
|
||||
color: 'error'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | undefined): string => {
|
||||
if (!dateString) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const parseNationalities = (nationalityString: string): string[] => {
|
||||
return nationalityString ? nationalityString.split(',').map(n => n.trim()).filter(n => n.length > 0) : [];
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string | undefined): string => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active': return 'success';
|
||||
case 'inactive': return 'error';
|
||||
case 'pending': return 'warning';
|
||||
case 'expired': return 'error';
|
||||
default: return 'grey';
|
||||
}
|
||||
};
|
||||
|
||||
const getTierColor = (tier: string | undefined): string => {
|
||||
switch (tier?.toLowerCase()) {
|
||||
case 'admin': return 'error';
|
||||
case 'board': return 'primary';
|
||||
case 'user': return 'info';
|
||||
default: return 'grey';
|
||||
}
|
||||
};
|
||||
|
||||
const getDaysRemainingColor = (days: number): string => {
|
||||
if (days < 0) return 'text-error font-weight-bold';
|
||||
if (days < 30) return 'text-warning font-weight-bold';
|
||||
return 'text-success';
|
||||
};
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
loadMemberData();
|
||||
});
|
||||
|
||||
// Watch for user changes
|
||||
watch(user, () => {
|
||||
if (user.value) {
|
||||
loadMemberData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.member-id-card {
|
||||
background: linear-gradient(135deg, rgba(163, 21, 21, 0.05) 0%, rgba(163, 21, 21, 0.1) 100%);
|
||||
border-left: 4px solid #a31515;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.v-card:hover {
|
||||
transform: translateY(-2px);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
color: #a31515 !important;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.member-id-card .d-flex {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.member-id-card .d-flex > * {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -162,8 +162,7 @@ const { firstName, user, userTier } = useAuth();
|
||||
|
||||
// Navigation methods (placeholder implementations)
|
||||
const navigateToProfile = () => {
|
||||
// TODO: Implement profile navigation
|
||||
console.log('Navigate to profile');
|
||||
navigateTo('/dashboard/profile');
|
||||
};
|
||||
|
||||
const navigateToEvents = () => {
|
||||
@@ -177,8 +176,19 @@ const navigateToResources = () => {
|
||||
};
|
||||
|
||||
const contactSupport = () => {
|
||||
// TODO: Implement support contact
|
||||
console.log('Contact support');
|
||||
const subject = encodeURIComponent('MonacoUSA Portal Support Request');
|
||||
const body = encodeURIComponent(`Hello,
|
||||
|
||||
I need assistance with:
|
||||
|
||||
[Please describe your issue]
|
||||
|
||||
Member: ${user.value?.name || 'Not provided'}
|
||||
Email: ${user.value?.email || 'Not provided'}
|
||||
|
||||
Thank you!`);
|
||||
|
||||
window.open(`mailto:support@monacousa.org?subject=${subject}&body=${body}`, '_self');
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user