283 lines
6.8 KiB
Vue
283 lines
6.8 KiB
Vue
|
|
<template>
|
||
|
|
<v-banner
|
||
|
|
v-if="event"
|
||
|
|
:color="bannerColor"
|
||
|
|
lines="two"
|
||
|
|
elevation="2"
|
||
|
|
rounded
|
||
|
|
>
|
||
|
|
<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>
|
||
|
|
</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>
|
||
|
|
|
||
|
|
<!-- 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>
|
||
|
|
</template>
|
||
|
|
</v-banner>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import type { Event, EventRSVP } from '~/utils/types';
|
||
|
|
import { format, isWithinInterval, addDays } from 'date-fns';
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
event: Event | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const props = defineProps<Props>();
|
||
|
|
|
||
|
|
const emit = defineEmits<{
|
||
|
|
'event-click': [event: Event];
|
||
|
|
'quick-rsvp': [event: Event];
|
||
|
|
}>();
|
||
|
|
|
||
|
|
// Computed properties
|
||
|
|
const userRSVP = computed((): EventRSVP | null => {
|
||
|
|
return props.event?.user_rsvp || null;
|
||
|
|
});
|
||
|
|
|
||
|
|
const canRSVP = computed(() => {
|
||
|
|
if (!props.event) return false;
|
||
|
|
const eventDate = new Date(props.event.start_datetime);
|
||
|
|
const now = new Date();
|
||
|
|
return eventDate > now; // Can RSVP to future events
|
||
|
|
});
|
||
|
|
|
||
|
|
const eventIcon = computed(() => {
|
||
|
|
if (!props.event) return 'mdi-calendar';
|
||
|
|
|
||
|
|
const icons = {
|
||
|
|
'meeting': 'mdi-account-group',
|
||
|
|
'social': 'mdi-party-popper',
|
||
|
|
'fundraiser': 'mdi-heart',
|
||
|
|
'workshop': 'mdi-school',
|
||
|
|
'board-only': 'mdi-shield-account'
|
||
|
|
};
|
||
|
|
|
||
|
|
return icons[props.event.event_type as keyof typeof icons] || 'mdi-calendar';
|
||
|
|
});
|
||
|
|
|
||
|
|
const bannerColor = computed(() => {
|
||
|
|
if (!props.event) return 'primary';
|
||
|
|
|
||
|
|
// Check if event is soon (within 24 hours)
|
||
|
|
const eventDate = new Date(props.event.start_datetime);
|
||
|
|
const now = new Date();
|
||
|
|
const isSoon = isWithinInterval(eventDate, {
|
||
|
|
start: now,
|
||
|
|
end: addDays(now, 1)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (isSoon) return 'warning';
|
||
|
|
|
||
|
|
const colors = {
|
||
|
|
'meeting': 'blue',
|
||
|
|
'social': 'green',
|
||
|
|
'fundraiser': 'orange',
|
||
|
|
'workshop': 'purple',
|
||
|
|
'board-only': 'red'
|
||
|
|
};
|
||
|
|
|
||
|
|
return colors[props.event.event_type as keyof typeof colors] || 'primary';
|
||
|
|
});
|
||
|
|
|
||
|
|
const iconColor = computed(() => {
|
||
|
|
// Use white for better contrast on colored backgrounds
|
||
|
|
return 'white';
|
||
|
|
});
|
||
|
|
|
||
|
|
const formatEventDate = computed(() => {
|
||
|
|
if (!props.event) return '';
|
||
|
|
|
||
|
|
const startDate = new Date(props.event.start_datetime);
|
||
|
|
const endDate = new Date(props.event.end_datetime);
|
||
|
|
const now = new Date();
|
||
|
|
|
||
|
|
// Different formats based on timing
|
||
|
|
if (startDate.toDateString() === now.toDateString()) {
|
||
|
|
return `Today at ${format(startDate, 'HH:mm')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (startDate.toDateString() === addDays(now, 1).toDateString()) {
|
||
|
|
return `Tomorrow at ${format(startDate, 'HH:mm')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (startDate.toDateString() === endDate.toDateString()) {
|
||
|
|
return format(startDate, 'EEE, MMM d • HH:mm');
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`;
|
||
|
|
});
|
||
|
|
|
||
|
|
const memberPrice = computed(() => {
|
||
|
|
if (!props.event || props.event.is_paid !== 'true') return '';
|
||
|
|
|
||
|
|
if (props.event.cost_members && props.event.cost_non_members) {
|
||
|
|
return `€${props.event.cost_members} (Members)`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return `€${props.event.cost_members || props.event.cost_non_members}`;
|
||
|
|
});
|
||
|
|
|
||
|
|
const capacityInfo = computed(() => {
|
||
|
|
if (!props.event?.max_attendees) return '';
|
||
|
|
|
||
|
|
const current = props.event.current_attendees || 0;
|
||
|
|
const max = parseInt(props.event.max_attendees);
|
||
|
|
|
||
|
|
return `${current}/${max} attending`;
|
||
|
|
});
|
||
|
|
|
||
|
|
const rsvpStatusColor = computed(() => {
|
||
|
|
const status = userRSVP.value?.rsvp_status;
|
||
|
|
switch (status) {
|
||
|
|
case 'confirmed': return 'success';
|
||
|
|
case 'waitlist': return 'warning';
|
||
|
|
case 'declined': return 'error';
|
||
|
|
default: return 'info';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const rsvpStatusIcon = computed(() => {
|
||
|
|
const status = userRSVP.value?.rsvp_status;
|
||
|
|
switch (status) {
|
||
|
|
case 'confirmed': return 'mdi-check';
|
||
|
|
case 'waitlist': return 'mdi-clock';
|
||
|
|
case 'declined': return 'mdi-close';
|
||
|
|
default: return 'mdi-help';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const rsvpStatusText = computed(() => {
|
||
|
|
const status = userRSVP.value?.rsvp_status;
|
||
|
|
switch (status) {
|
||
|
|
case 'confirmed': return 'Attending';
|
||
|
|
case 'waitlist': return 'Waitlisted';
|
||
|
|
case 'declined': return 'Declined';
|
||
|
|
default: return 'Unknown';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const quickRSVPColor = computed(() => {
|
||
|
|
return bannerColor.value === 'warning' ? 'success' : 'white';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Methods
|
||
|
|
const handleViewEvent = () => {
|
||
|
|
if (props.event) {
|
||
|
|
emit('event-click', props.event);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleQuickRSVP = () => {
|
||
|
|
if (props.event) {
|
||
|
|
emit('quick-rsvp', props.event);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.v-banner :deep(.v-banner__wrapper) {
|
||
|
|
padding: 16px 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.v-banner :deep(.v-banner__prepend) {
|
||
|
|
margin-inline-end: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.v-banner :deep(.v-banner__actions) {
|
||
|
|
margin-inline-start: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Mobile optimizations */
|
||
|
|
@media (max-width: 600px) {
|
||
|
|
.v-banner :deep(.v-banner__wrapper) {
|
||
|
|
padding: 12px 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.v-banner :deep(.v-banner__prepend) {
|
||
|
|
margin-inline-end: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.v-banner :deep(.v-banner__actions) {
|
||
|
|
margin-inline-start: 0;
|
||
|
|
margin-top: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.text-h6 {
|
||
|
|
font-size: 1.1rem !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Ensure proper spacing on different screen sizes */
|
||
|
|
.ga-4 {
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.ga-2 {
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 600px) {
|
||
|
|
.ga-4 {
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|