feat: Reorganize platform into member, board, and admin sections
Some checks failed
Build And Push Image / docker (push) Failing after 55s
Some checks failed
Build And Push Image / docker (push) Failing after 55s
Major platform reorganization implementing role-based portal sections: ## Infrastructure Changes - Created role-based middleware for member, board, and admin access - Updated main dashboard router to redirect based on highest privilege - Implemented access hierarchy: Admin > Board > Member ## New Layouts - Member layout: Simplified navigation for regular members - Board layout: Enhanced tools for board member management - Admin layout: Full system administration capabilities ## Member Portal (/member/*) - Dashboard: Profile overview, events, payments, activity tracking - Events: Browse, register, and manage event participation - Profile: Complete personal and professional information management - Resources: Access to documents, guides, FAQs, and quick links ## Board Portal (/board/*) - Dashboard: Statistics, dues management, board-specific tools - Members: Comprehensive member management with filtering ## Admin Portal (/admin/*) - Dashboard: System overview and administrative controls (existing) ## Design Implementation - Monaco red (#dc2626) as primary accent color - Modern card-based layouts with consistent spacing - Responsive design for all screen sizes - Glass morphism effects for enhanced visual appeal This reorganization provides clear separation of concerns based on user privileges while maintaining a cohesive user experience across all sections. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
552
pages/member/events/index.vue
Normal file
552
pages/member/events/index.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-h4 font-weight-bold mb-2">Events</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Discover and register for upcoming MonacoUSA events</p>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<v-card class="mb-6" elevation="1">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="Search events"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="selectedCategory"
|
||||
:items="categories"
|
||||
label="Category"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="selectedMonth"
|
||||
:items="months"
|
||||
label="Month"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="flat"
|
||||
block
|
||||
@click="resetFilters"
|
||||
>
|
||||
Reset Filters
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Event Tabs -->
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
color="error"
|
||||
class="mb-6"
|
||||
>
|
||||
<v-tab value="upcoming">
|
||||
<v-icon start>mdi-calendar-clock</v-icon>
|
||||
Upcoming Events
|
||||
</v-tab>
|
||||
<v-tab value="registered">
|
||||
<v-icon start>mdi-calendar-check</v-icon>
|
||||
My Registrations
|
||||
</v-tab>
|
||||
<v-tab value="past">
|
||||
<v-icon start>mdi-history</v-icon>
|
||||
Past Events
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<v-window v-model="tab">
|
||||
<!-- Upcoming Events Tab -->
|
||||
<v-window-item value="upcoming">
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="event in upcomingEvents"
|
||||
:key="event.id"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card elevation="2" hover class="h-100 d-flex flex-column">
|
||||
<!-- Event Image -->
|
||||
<v-img
|
||||
:src="event.image"
|
||||
height="200"
|
||||
cover
|
||||
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
|
||||
>
|
||||
<v-card-title class="text-white">
|
||||
{{ event.title }}
|
||||
</v-card-title>
|
||||
</v-img>
|
||||
|
||||
<v-card-text class="flex-grow-1">
|
||||
<!-- Event Details -->
|
||||
<div class="mb-3">
|
||||
<v-chip
|
||||
:color="getCategoryColor(event.category)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ event.category }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<p class="text-body-2 mb-3">{{ event.description }}</p>
|
||||
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="x-small" class="mr-2">mdi-calendar</v-icon>
|
||||
{{ formatDate(event.date) }}
|
||||
</div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="x-small" class="mr-2">mdi-clock-outline</v-icon>
|
||||
{{ event.time }}
|
||||
</div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="x-small" class="mr-2">mdi-map-marker</v-icon>
|
||||
{{ event.location }}
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="x-small" class="mr-2">mdi-account-group</v-icon>
|
||||
{{ event.attendees }} / {{ event.capacity }} attendees
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="viewEventDetails(event)"
|
||||
>
|
||||
Learn More
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="error"
|
||||
:disabled="event.attendees >= event.capacity"
|
||||
@click="registerForEvent(event)"
|
||||
>
|
||||
{{ event.attendees >= event.capacity ? 'Full' : 'Register' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Empty State -->
|
||||
<v-card v-if="upcomingEvents.length === 0" class="text-center pa-8" elevation="0">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-calendar-blank</v-icon>
|
||||
<h3 class="text-h6 mt-4">No upcoming events</h3>
|
||||
<p class="text-body-2 text-medium-emphasis">Check back later for new events</p>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Registered Events Tab -->
|
||||
<v-window-item value="registered">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card elevation="1">
|
||||
<v-list lines="three">
|
||||
<v-list-item
|
||||
v-for="registration in registeredEvents"
|
||||
:key="registration.id"
|
||||
class="py-3"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="60" rounded="lg">
|
||||
<v-img :src="registration.image" cover />
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-medium">
|
||||
{{ registration.title }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<div class="d-flex gap-3 mt-1">
|
||||
<span><v-icon size="x-small">mdi-calendar</v-icon> {{ formatDate(registration.date) }}</span>
|
||||
<span><v-icon size="x-small">mdi-clock-outline</v-icon> {{ registration.time }}</span>
|
||||
<span><v-icon size="x-small">mdi-map-marker</v-icon> {{ registration.location }}</span>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="text-right">
|
||||
<v-chip
|
||||
color="success"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-icon start size="x-small">mdi-check</v-icon>
|
||||
Registered
|
||||
</v-chip>
|
||||
<div>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="cancelRegistration(registration)"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="registeredEvents.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-calendar-remove</v-icon>
|
||||
<h3 class="text-h6 mt-4">No registered events</h3>
|
||||
<p class="text-body-2 text-medium-emphasis">Browse upcoming events to find something interesting</p>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Past Events Tab -->
|
||||
<v-window-item value="past">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-timeline side="end" density="compact">
|
||||
<v-timeline-item
|
||||
v-for="event in pastEvents"
|
||||
:key="event.id"
|
||||
dot-color="grey"
|
||||
size="small"
|
||||
>
|
||||
<template v-slot:opposite>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ formatDate(event.date) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-card elevation="1">
|
||||
<v-card-title class="text-h6">{{ event.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-2 mb-2">{{ event.description }}</p>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
<v-icon size="x-small">mdi-account-group</v-icon>
|
||||
{{ event.attendees }} attendees
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="viewEventPhotos(event)"
|
||||
>
|
||||
View Photos
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="viewEventDetails(event)"
|
||||
>
|
||||
View Details
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
|
||||
<!-- Empty State -->
|
||||
<v-card v-if="pastEvents.length === 0" class="text-center pa-8" elevation="0">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-history</v-icon>
|
||||
<h3 class="text-h6 mt-4">No past events</h3>
|
||||
<p class="text-body-2 text-medium-emphasis">Past events will appear here</p>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<!-- Event Details Dialog -->
|
||||
<v-dialog v-model="detailsDialog" max-width="600">
|
||||
<v-card v-if="selectedEvent">
|
||||
<v-img
|
||||
:src="selectedEvent.image"
|
||||
height="200"
|
||||
cover
|
||||
/>
|
||||
<v-card-title>{{ selectedEvent.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-chip
|
||||
:color="getCategoryColor(selectedEvent.category)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="mb-3"
|
||||
>
|
||||
{{ selectedEvent.category }}
|
||||
</v-chip>
|
||||
|
||||
<p class="mb-4">{{ selectedEvent.fullDescription || selectedEvent.description }}</p>
|
||||
|
||||
<v-list density="compact">
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-calendar</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ formatDate(selectedEvent.date) }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-clock-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ selectedEvent.time }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-map-marker</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ selectedEvent.location }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-account-group</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ selectedEvent.attendees }} / {{ selectedEvent.capacity }} attendees</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="detailsDialog = false">Close</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="flat"
|
||||
:disabled="selectedEvent.attendees >= selectedEvent.capacity"
|
||||
@click="registerForEvent(selectedEvent)"
|
||||
>
|
||||
Register
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'member',
|
||||
middleware: 'member'
|
||||
});
|
||||
|
||||
// State
|
||||
const tab = ref('upcoming');
|
||||
const searchQuery = ref('');
|
||||
const selectedCategory = ref(null);
|
||||
const selectedMonth = ref(null);
|
||||
const detailsDialog = ref(false);
|
||||
const selectedEvent = ref(null);
|
||||
|
||||
// Filter options
|
||||
const categories = ref([
|
||||
'Networking',
|
||||
'Workshop',
|
||||
'Social',
|
||||
'Cultural',
|
||||
'Business',
|
||||
'Charity'
|
||||
]);
|
||||
|
||||
const months = ref([
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
]);
|
||||
|
||||
// Mock event data
|
||||
const upcomingEvents = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Monaco Business Networking',
|
||||
description: 'Connect with fellow Monaco entrepreneurs and business leaders',
|
||||
fullDescription: 'Join us for an evening of networking with Monaco\'s business community. This event brings together entrepreneurs, executives, and professionals from various industries.',
|
||||
category: 'Networking',
|
||||
date: '2024-02-15',
|
||||
time: '6:00 PM - 8:00 PM',
|
||||
location: 'Monaco Yacht Club',
|
||||
image: 'https://picsum.photos/400/300?random=1',
|
||||
attendees: 45,
|
||||
capacity: 100
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Digital Marketing Workshop',
|
||||
description: 'Learn the latest digital marketing strategies and techniques',
|
||||
category: 'Workshop',
|
||||
date: '2024-02-22',
|
||||
time: '2:00 PM - 5:00 PM',
|
||||
location: 'Conference Center',
|
||||
image: 'https://picsum.photos/400/300?random=2',
|
||||
attendees: 28,
|
||||
capacity: 50
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Annual Gala Dinner',
|
||||
description: 'Celebrate the year with an elegant evening of dining and entertainment',
|
||||
category: 'Social',
|
||||
date: '2024-03-05',
|
||||
time: '7:00 PM - 11:00 PM',
|
||||
location: 'Hotel Hermitage',
|
||||
image: 'https://picsum.photos/400/300?random=3',
|
||||
attendees: 120,
|
||||
capacity: 150
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Monaco Grand Prix Viewing',
|
||||
description: 'Watch the Monaco Grand Prix from our exclusive viewing area',
|
||||
category: 'Social',
|
||||
date: '2024-05-26',
|
||||
time: '12:00 PM - 6:00 PM',
|
||||
location: 'Private Terrace',
|
||||
image: 'https://picsum.photos/400/300?random=4',
|
||||
attendees: 75,
|
||||
capacity: 75
|
||||
}
|
||||
]);
|
||||
|
||||
const registeredEvents = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Monaco Business Networking',
|
||||
date: '2024-02-15',
|
||||
time: '6:00 PM',
|
||||
location: 'Monaco Yacht Club',
|
||||
image: 'https://picsum.photos/400/300?random=1'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Annual Gala Dinner',
|
||||
date: '2024-03-05',
|
||||
time: '7:00 PM',
|
||||
location: 'Hotel Hermitage',
|
||||
image: 'https://picsum.photos/400/300?random=3'
|
||||
}
|
||||
]);
|
||||
|
||||
const pastEvents = ref([
|
||||
{
|
||||
id: 5,
|
||||
title: 'New Year Celebration',
|
||||
description: 'Welcomed 2024 with a spectacular celebration',
|
||||
date: '2024-01-01',
|
||||
attendees: 200
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Investment Seminar',
|
||||
description: 'Expert insights on investment strategies for 2024',
|
||||
date: '2024-01-15',
|
||||
attendees: 65
|
||||
}
|
||||
]);
|
||||
|
||||
// Methods
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
'Networking': 'blue',
|
||||
'Workshop': 'purple',
|
||||
'Social': 'green',
|
||||
'Cultural': 'orange',
|
||||
'Business': 'indigo',
|
||||
'Charity': 'pink'
|
||||
};
|
||||
return colors[category] || 'grey';
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
searchQuery.value = '';
|
||||
selectedCategory.value = null;
|
||||
selectedMonth.value = null;
|
||||
};
|
||||
|
||||
const viewEventDetails = (event: any) => {
|
||||
selectedEvent.value = event;
|
||||
detailsDialog.value = true;
|
||||
};
|
||||
|
||||
const registerForEvent = (event: any) => {
|
||||
console.log('Registering for event:', event.title);
|
||||
// Add to registered events
|
||||
if (!registeredEvents.value.find(e => e.id === event.id)) {
|
||||
registeredEvents.value.push({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
date: event.date,
|
||||
time: event.time,
|
||||
location: event.location,
|
||||
image: event.image
|
||||
});
|
||||
}
|
||||
detailsDialog.value = false;
|
||||
};
|
||||
|
||||
const cancelRegistration = (event: any) => {
|
||||
console.log('Canceling registration for:', event.title);
|
||||
const index = registeredEvents.value.findIndex(e => e.id === event.id);
|
||||
if (index > -1) {
|
||||
registeredEvents.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const viewEventPhotos = (event: any) => {
|
||||
console.log('Viewing photos for:', event.title);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user