monacousa-portal/layouts/member.vue

656 lines
17 KiB
Vue

<template>
<v-app style="background-color: #fafafa;">
<v-navigation-drawer
v-model="drawer"
:rail="miniVariant"
:expand-on-hover="false"
permanent
width="280"
rail-width="100"
class="enhanced-glass-drawer"
>
<!-- Logo Section with Enhanced Glass Effect -->
<v-list-item class="logo-section">
<template v-if="!miniVariant">
<v-img
src="/MONACOUSA-Flags_376x376.png"
width="80"
height="80"
class="mx-auto logo-image mb-2"
/>
<div class="logo-text">
<div class="text-h6 font-weight-bold monaco-red-text">
MonacoUSA Portal
</div>
<v-chip
size="x-small"
class="monaco-chip-gradient mt-1"
>
MEMBER
</v-chip>
</div>
</template>
<template v-else>
<v-img
src="/MONACOUSA-Flags_376x376.png"
width="40"
height="40"
class="mx-auto logo-image"
/>
</template>
</v-list-item>
<v-divider class="glass-divider" />
<!-- Navigation Menu with Enhanced Effects -->
<v-list nav class="enhanced-nav-list">
<template v-for="item in navigationItems" :key="item.value">
<v-tooltip
:text="item.title"
location="right"
:disabled="!miniVariant"
>
<template v-slot:activator="{ props }">
<v-list-item
:to="item.to"
:prepend-icon="item.icon"
:title="!miniVariant ? item.title : undefined"
:value="item.value"
class="nav-item-enhanced"
v-bind="props"
>
<template v-if="item.badge" v-slot:append>
<v-badge
:content="item.badge"
color="error"
inline
:dot="miniVariant"
/>
</template>
</v-list-item>
</template>
</v-tooltip>
</template>
</v-list>
<!-- Enhanced Profile Footer -->
<template v-slot:append>
<div class="pa-2">
<v-card class="glass-profile-card overflow-visible">
<div class="d-flex align-center" :class="miniVariant ? 'flex-column py-3 px-2' : 'pa-3'">
<!-- Avatar Section -->
<div class="position-relative">
<ProfileAvatar
v-if="memberData"
:member-id="memberData?.member_id"
:first-name="memberData?.first_name || user?.firstName"
:last-name="memberData?.last_name || user?.lastName"
:size="miniVariant ? '32' : 'small'"
:class="miniVariant ? '' : 'mr-3'"
:show-badge="false"
/>
<div v-if="!miniVariant" class="online-indicator" />
</div>
<!-- Info Section (Hidden in mini mode) -->
<div v-if="!miniVariant" class="flex-grow-1">
<div class="text-subtitle-2 font-weight-bold">{{ fullName }}</div>
<div class="text-caption text-medium-emphasis">{{ email?.split('@')[0] || 'member' }}</div>
<v-chip size="x-small" class="mt-1 glass-badge">Member</v-chip>
</div>
<!-- Action Buttons -->
<div :class="miniVariant ? 'mt-2' : 'ml-auto'">
<v-menu location="top" offset-y>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
:size="miniVariant ? 'small' : 'small'"
variant="text"
class="profile-menu-btn"
>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list density="compact" class="glass-menu" min-width="200">
<v-list-item to="/member/profile" class="hover-lift">
<template v-slot:prepend>
<v-icon size="small" color="primary">mdi-account-circle</v-icon>
</template>
<v-list-item-title>My Profile</v-list-item-title>
</v-list-item>
<v-list-item to="/member/settings" class="hover-lift">
<template v-slot:prepend>
<v-icon size="small" color="info">mdi-cog-outline</v-icon>
</template>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
<v-list-item @click="() => {}" class="hover-lift">
<template v-slot:prepend>
<v-icon size="small" color="warning">mdi-bell-outline</v-icon>
</template>
<v-list-item-title>Notifications</v-list-item-title>
</v-list-item>
<v-divider class="my-1 glass-divider" />
<v-list-item @click="handleLogout" class="hover-lift">
<template v-slot:prepend>
<v-icon size="small" color="error">mdi-logout-variant</v-icon>
</template>
<v-list-item-title class="text-error">Sign Out</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</div>
</v-card>
</div>
</template>
</v-navigation-drawer>
<v-app-bar elevation="0" flat class="glass-app-bar member-bar">
<v-btn
icon
@click="toggleDrawer"
class="glass-icon-btn mr-2"
>
<v-icon>{{ miniVariant ? 'mdi-menu' : 'mdi-menu-open' }}</v-icon>
</v-btn>
<v-toolbar-title class="font-weight-bold text-white">
Member Portal
</v-toolbar-title>
<v-spacer />
<!-- Quick Actions with Glass Effects -->
<v-btn icon class="glass-icon-btn">
<v-badge
:content="notifications"
:value="notifications > 0"
color="error"
>
<v-icon>mdi-bell</v-icon>
</v-badge>
</v-btn>
<!-- User Menu -->
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props" class="glass-icon-btn">
<ProfileAvatar
:member-id="memberData?.member_id"
:member-name="user?.name"
:first-name="user?.firstName || memberData?.first_name"
:last-name="user?.lastName || memberData?.last_name"
size="small"
:lazy="false"
show-border
/>
</v-btn>
</template>
<v-list min-width="250" class="glass-dropdown">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ user?.name || 'Member' }}
</v-list-item-title>
<v-list-item-subtitle>
{{ user?.email }}
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-chip
size="x-small"
class="monaco-chip-gradient"
>
MEMBER
</v-chip>
</v-list-item>
<v-divider class="my-2 glass-divider" />
<v-list-item to="/member/profile" class="glass-dropdown-item">
<template v-slot:prepend>
<v-icon>mdi-account</v-icon>
</template>
<v-list-item-title>My Profile</v-list-item-title>
</v-list-item>
<v-list-item to="/member/settings" class="glass-dropdown-item">
<template v-slot:prepend>
<v-icon>mdi-cog</v-icon>
</template>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
<v-divider class="my-2 glass-divider" />
<v-list-item @click="handleLogout" class="glass-dropdown-item text-error">
<template v-slot:prepend>
<v-icon color="error">mdi-logout</v-icon>
</template>
<v-list-item-title>Logout</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-app-bar>
<v-main class="glass-main">
<!-- Dues Payment Banner with Glass Effect -->
<DuesPaymentBanner />
<v-container fluid class="pa-6">
<slot />
</v-container>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import type { Member } from '~/utils/types';
const { user, logout } = useAuth();
const drawer = ref(true);
const miniVariant = ref(false);
const notifications = ref(0);
// Navigation items configuration
const navigationItems = ref([
{
to: '/member/dashboard',
icon: 'mdi-view-dashboard',
title: 'Dashboard',
value: 'dashboard'
},
{
to: '/member/profile',
icon: 'mdi-account',
title: 'My Profile',
value: 'profile'
},
{
to: '/member/events',
icon: 'mdi-calendar',
title: 'Events',
value: 'events',
badge: '3' // Example badge
},
{
to: '/member/payments',
icon: 'mdi-credit-card',
title: 'Payments & Dues',
value: 'payments'
},
{
to: '/member/resources',
icon: 'mdi-book-open-variant',
title: 'Resources',
value: 'resources'
}
]);
// Fetch member data
const { data: sessionData } = await useFetch<{ success: boolean; member: Member | null }>('/api/auth/session', {
server: false
});
const memberData = computed<Member | null>(() => sessionData.value?.member || null);
// Computed properties for profile
const fullName = computed(() => {
if (memberData.value) {
return `${memberData.value.first_name} ${memberData.value.last_name}`;
}
return user.value?.name || 'Member';
});
const email = computed(() => memberData.value?.email || user.value?.email || '');
// Check for notifications
onMounted(async () => {
// Check for upcoming events, dues reminders, etc.
try {
const { data } = await $fetch('/api/member/notifications/count');
notifications.value = data?.count || 0;
} catch (error) {
console.error('Error fetching notifications:', error);
}
});
const toggleDrawer = () => {
miniVariant.value = !miniVariant.value;
};
const handleLogout = async () => {
await logout();
};
// Responsive drawer behavior
const { width } = useDisplay();
watch(width, (newWidth) => {
drawer.value = newWidth >= 1024;
}, { immediate: true });
</script>
<style scoped lang="scss">
@import '~/assets/scss/main.scss';
// Enhanced Glass Drawer Styles
.enhanced-glass-drawer {
@include enhanced-glass(0.95, 30px);
border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.logo-section {
position: relative;
padding: 1.5rem;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.95) 0%,
rgba(248, 248, 248, 0.9) 100%);
border-radius: 16px;
margin: 0.5rem;
margin-bottom: 1rem;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
&--collapsed {
padding: 0.75rem;
.logo-image {
margin: 0 auto;
}
}
.logo-image {
transition: all 0.3s ease;
}
.logo-text {
text-align: center;
}
.collapse-btn {
position: absolute;
right: -0.5rem;
top: 50%;
transform: translateY(-50%);
background: white;
border: 1px solid rgba(220, 38, 38, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
background: rgba(220, 38, 38, 0.05);
}
}
}
// Monaco Text Colors
.monaco-red-text {
color: #dc2626 !important;
}
// Enhanced Navigation Items
.enhanced-nav-list {
background: transparent !important;
padding: 0.5rem;
}
.nav-item-enhanced {
border-radius: 12px !important;
margin: 4px 8px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative;
overflow: hidden;
@include ripple-effect();
&:hover {
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.08) 0%,
rgba(220, 38, 38, 0.04) 100%) !important;
transform: translateX(4px);
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg,
transparent 0%,
rgba(220, 38, 38, 0.05) 50%,
transparent 100%);
animation: shimmer 1s ease-in-out;
}
}
&.v-list-item--active {
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.15) 0%,
rgba(220, 38, 38, 0.08) 100%) !important;
color: #dc2626 !important;
@include sliding-indicator();
.v-icon {
color: #dc2626 !important;
animation: pulse 2s ease-in-out infinite;
}
}
.v-list-item__prepend {
.v-icon {
transition: all 0.3s ease;
}
}
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
// Glass Divider
.glass-divider {
opacity: 0.2;
border-color: rgba(220, 38, 38, 0.2);
}
// Member App Bar with Gradient
.member-bar {
background: linear-gradient(135deg,
rgba(239, 68, 68, 0.9) 0%,
rgba(220, 38, 38, 0.9) 100%) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
// Glass Icon Buttons
.glass-icon-btn {
background: rgba(255, 255, 255, 0.1) !important;
backdrop-filter: blur(10px);
color: white !important;
transition: all 0.3s ease !important;
&:hover {
background: rgba(255, 255, 255, 0.2) !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
// Monaco Chip
.monaco-chip-gradient {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
color: white !important;
border: none;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.25);
}
// Glass Dropdown
.glass-dropdown {
@include glass-effect(0.95, 20px);
border-radius: 12px !important;
overflow: hidden;
}
.glass-dropdown-item {
transition: all 0.2s ease !important;
&:hover {
background: rgba(220, 38, 38, 0.05) !important;
}
}
// Glass Main Background
.glass-main {
background-color: #fafafa; // Solid fallback for Edge and other browsers
background-image: linear-gradient(180deg,
rgba(250, 250, 250, 0.9) 0%,
rgba(245, 245, 245, 0.9) 100%);
background: linear-gradient(180deg,
rgba(250, 250, 250, 0.9) 0%,
rgba(245, 245, 245, 0.9) 100%);
min-height: 100vh;
position: relative;
&::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 20% 50%, rgba(220, 38, 38, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(220, 38, 38, 0.03) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, rgba(220, 38, 38, 0.03) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
}
// Profile Footer Styles
.profile-footer {
padding: 0.5rem;
}
.profile-card-footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.9),
rgba(255, 255, 255, 0.7)
);
border-radius: 12px;
margin: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
&:hover {
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.95),
rgba(255, 255, 255, 0.85)
);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.profile-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: 100%;
position: relative;
.profile-menu-btn {
position: absolute;
top: -0.5rem;
right: -0.5rem;
opacity: 0.6;
transition: opacity 0.2s ease;
&:hover {
opacity: 1;
}
}
}
}
.profile-avatar-wrapper {
position: relative;
flex-shrink: 0;
.online-indicator {
position: absolute;
bottom: -2px;
right: -2px;
width: 10px;
height: 10px;
background: #22c55e;
border: 2px solid white;
border-radius: 50%;
animation: pulse-online 2s ease-in-out infinite;
}
}
.profile-info {
text-align: center;
width: 100%;
.profile-name {
font-size: 0.925rem;
font-weight: 600;
color: rgb(31, 41, 55);
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-email {
font-size: 0.8rem;
color: rgb(107, 114, 128);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@keyframes pulse-online {
0%, 100% {
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
}
50% {
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
}
}
// Fade transition
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
// Responsive adjustments
@media (max-width: 1024px) {
.nav-item-enhanced {
margin: 2px 8px !important;
}
}
</style>