fixes
All checks were successful
Build And Push Image / docker (push) Successful in 4m16s

This commit is contained in:
2025-08-13 15:57:34 +02:00
parent db19eb2708
commit 5371ad4fa2
4 changed files with 321 additions and 74 deletions

View File

@@ -25,14 +25,19 @@
v-model="mobileView"
color="primary"
variant="outlined"
density="compact"
density="comfortable"
mandatory
class="w-100"
>
<v-btn value="month">
<v-btn value="week" class="flex-grow-1">
<v-icon start>mdi-calendar-week</v-icon>
Week
</v-btn>
<v-btn value="month" class="flex-grow-1">
<v-icon start>mdi-calendar-month</v-icon>
Month
</v-btn>
<v-btn value="list">
<v-btn value="list" class="flex-grow-1">
<v-icon start>mdi-format-list-bulleted</v-icon>
Agenda
</v-btn>
@@ -108,7 +113,7 @@ const { isBoard, isAdmin } = useAuth();
// Reactive state
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
const mobileView = ref('month');
const mobileView = ref('week'); // Default to week view on mobile
// Computed properties
const calendarHeight = computed(() => {
@@ -122,7 +127,12 @@ const currentView = computed(() => {
// Mobile responsive view switching
if (process.client && window.innerWidth < 960) {
return mobileView.value === 'list' ? 'listWeek' : 'dayGridMonth';
switch (mobileView.value) {
case 'week': return 'dayGridWeek';
case 'list': return 'listWeek';
case 'month':
default: return 'dayGridMonth';
}
}
return props.initialView;
@@ -375,7 +385,13 @@ function gotoDate(date: string | Date) {
// Watch for mobile view changes
watch(mobileView, (newView) => {
const viewType = newView === 'list' ? 'listWeek' : 'dayGridMonth';
let viewType;
switch (newView) {
case 'week': viewType = 'dayGridWeek'; break;
case 'list': viewType = 'listWeek'; break;
case 'month':
default: viewType = 'dayGridMonth'; break;
}
changeView(viewType);
});

View File

@@ -1,75 +1,155 @@
<template>
<v-banner
<v-card
v-if="event"
:color="bannerColor"
lines="two"
elevation="2"
rounded
elevation="3"
class="upcoming-event-banner ma-2"
:color="eventTypeColor"
theme="dark"
rounded="xl"
>
<template #prepend>
<v-icon :color="iconColor" size="large">{{ eventIcon }}</v-icon>
</template>
<template #text>
<div class="d-flex flex-column">
<div class="text-h6 font-weight-bold mb-1">{{ event.title }}</div>
<div class="d-flex flex-wrap align-center ga-4 text-body-2">
<div class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-calendar-clock</v-icon>
<span>{{ formatEventDate }}</span>
</div>
<div v-if="event.location" class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-map-marker</v-icon>
<span>{{ event.location }}</span>
</div>
<div v-if="event.is_paid === 'true'" class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-currency-eur</v-icon>
<span>{{ memberPrice }}</span>
</div>
<div v-if="capacityInfo" class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-account-group</v-icon>
<span>{{ capacityInfo }}</span>
<v-card-text class="pa-4">
<!-- Mobile Layout -->
<div v-if="$vuetify.display.mobile" class="mobile-banner-layout">
<!-- Header -->
<div class="d-flex align-center mb-3">
<v-avatar :color="eventTypeColor" class="me-3" size="40">
<v-icon :icon="eventTypeIcon" size="20"></v-icon>
</v-avatar>
<div class="flex-grow-1">
<h3 class="text-h6 font-weight-bold text-truncate">{{ event.title }}</h3>
<div class="text-caption opacity-90">{{ eventTypeLabel }}</div>
</div>
</div>
</div>
</template>
<template #actions>
<div class="d-flex flex-column flex-sm-row ga-2">
<!-- RSVP Status -->
<v-chip
v-if="userRSVP"
:color="rsvpStatusColor"
size="small"
variant="flat"
>
<v-icon start size="small">{{ rsvpStatusIcon }}</v-icon>
{{ rsvpStatusText }}
</v-chip>
<!-- Event Details -->
<div class="mb-3">
<div class="d-flex align-center mb-1">
<v-icon size="16" class="me-2">mdi-calendar-clock</v-icon>
<span class="text-body-2">{{ formatEventDate }}</span>
</div>
<div v-if="event.location" class="d-flex align-center mb-1">
<v-icon size="16" class="me-2">mdi-map-marker</v-icon>
<span class="text-body-2 text-truncate">{{ event.location }}</span>
</div>
<div class="d-flex align-center justify-space-between">
<div v-if="event.is_paid === 'true'" class="d-flex align-center">
<v-icon size="16" class="me-2">mdi-currency-eur</v-icon>
<span class="text-body-2">
{{ memberPrice || nonMemberPrice }}
<span v-if="memberPrice && nonMemberPrice" class="text-caption">(Members)</span>
</span>
</div>
<div v-if="event.max_attendees" class="d-flex align-center">
<v-icon size="16" class="me-2">mdi-account-group</v-icon>
<span class="text-body-2">{{ event.current_attendees || 0 }}/{{ event.max_attendees }} attending</span>
</div>
</div>
</div>
<!-- Action Buttons -->
<v-btn
@click="handleViewEvent"
variant="elevated"
color="white"
size="small"
prepend-icon="mdi-eye"
>
View Details
</v-btn>
<v-btn
v-if="!userRSVP && canRSVP"
@click="handleQuickRSVP"
:color="quickRSVPColor"
size="small"
prepend-icon="mdi-check"
>
Quick RSVP
</v-btn>
<div class="d-flex ga-2">
<v-btn
@click="handleQuickRSVP"
:color="userRSVP ? 'success' : 'white'"
:variant="userRSVP ? 'elevated' : 'outlined'"
size="small"
class="text-none flex-grow-1"
rounded="lg"
>
<v-icon start size="18">
{{ userRSVP ? 'mdi-check' : 'mdi-plus' }}
</v-icon>
{{ userRSVP ? 'Attending' : 'Quick RSVP' }}
</v-btn>
<v-btn
@click="handleViewDetails"
color="white"
variant="outlined"
size="small"
class="text-none"
rounded="lg"
icon
>
<v-icon size="18">mdi-eye</v-icon>
</v-btn>
</div>
</div>
</template>
</v-banner>
<!-- Desktop Layout -->
<v-row v-else align="center" no-gutters>
<v-col cols="12" md="8">
<div class="d-flex align-center mb-2">
<v-avatar :color="eventTypeColor" class="me-3" size="32">
<v-icon :icon="eventTypeIcon" size="16"></v-icon>
</v-avatar>
<div>
<h3 class="text-h6 font-weight-bold">{{ event.title }}</h3>
<div class="text-caption opacity-90">{{ eventTypeLabel }}</div>
</div>
</div>
<div class="d-flex align-center flex-wrap ga-4">
<div class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-calendar-clock</v-icon>
<span class="text-body-2">{{ formatEventDate }}</span>
</div>
<div v-if="event.location" class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-map-marker</v-icon>
<span class="text-body-2">{{ event.location }}</span>
</div>
<div v-if="event.is_paid === 'true'" class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-currency-eur</v-icon>
<span class="text-body-2">
{{ memberPrice || nonMemberPrice }}
<span v-if="memberPrice && nonMemberPrice">(Members)</span>
</span>
</div>
<div v-if="event.max_attendees" class="d-flex align-center">
<v-icon size="small" class="me-1">mdi-account-group</v-icon>
<span class="text-body-2">{{ event.current_attendees || 0 }}/{{ event.max_attendees }} attending</span>
</div>
</div>
</v-col>
<v-col cols="12" md="4" class="text-end">
<div class="d-flex ga-2 justify-end">
<v-btn
@click="handleQuickRSVP"
:color="userRSVP ? 'success' : 'white'"
:variant="userRSVP ? 'elevated' : 'outlined'"
size="small"
class="text-none"
rounded="lg"
>
<v-icon start size="small">
{{ userRSVP ? 'mdi-check' : 'mdi-plus' }}
</v-icon>
{{ userRSVP ? 'Attending' : 'Quick RSVP' }}
</v-btn>
<v-btn
@click="handleViewDetails"
color="white"
variant="outlined"
size="small"
class="text-none"
rounded="lg"
>
<v-icon start size="small">mdi-eye</v-icon>
View Details
</v-btn>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
@@ -99,7 +179,7 @@ const canRSVP = computed(() => {
return eventDate > now; // Can RSVP to future events
});
const eventIcon = computed(() => {
const eventTypeIcon = computed(() => {
if (!props.event) return 'mdi-calendar';
const icons = {
@@ -113,7 +193,7 @@ const eventIcon = computed(() => {
return icons[props.event.event_type as keyof typeof icons] || 'mdi-calendar';
});
const bannerColor = computed(() => {
const eventTypeColor = computed(() => {
if (!props.event) return 'primary';
// Check if event is soon (within 24 hours)
@@ -137,11 +217,27 @@ const bannerColor = computed(() => {
return colors[props.event.event_type as keyof typeof colors] || 'primary';
});
const eventTypeLabel = computed(() => {
if (!props.event) return '';
const labels = {
'meeting': 'Meeting',
'social': 'Social Event',
'fundraiser': 'Fundraiser',
'workshop': 'Workshop',
'board-only': 'Board Only'
};
return labels[props.event.event_type as keyof typeof labels] || 'Event';
});
const iconColor = computed(() => {
// Use white for better contrast on colored backgrounds
return 'white';
});
const nonMemberPrice = computed(() => props.event?.cost_non_members || '');
const formatEventDate = computed(() => {
if (!props.event) return '';
@@ -215,7 +311,7 @@ const rsvpStatusText = computed(() => {
});
const quickRSVPColor = computed(() => {
return bannerColor.value === 'warning' ? 'success' : 'white';
return eventTypeColor.value === 'warning' ? 'success' : 'white';
});
// Methods
@@ -225,6 +321,12 @@ const handleViewEvent = () => {
}
};
const handleViewDetails = () => {
if (props.event) {
emit('event-click', props.event);
}
};
const handleQuickRSVP = () => {
if (props.event) {
emit('quick-rsvp', props.event);