Initial commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
event: any | null
|
||||
position: { x: number; y: number }
|
||||
visible: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'click': [event: any]
|
||||
}>()
|
||||
|
||||
const formatDateTime = (isoString: string | null | undefined): string => {
|
||||
if (!isoString) return 'Not set'
|
||||
const date = new Date(isoString)
|
||||
return date.toLocaleString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const formatTime = (isoString: string | null | undefined): string => {
|
||||
if (!isoString) return 'Not set'
|
||||
const date = new Date(isoString)
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const popupStyle = computed(() => {
|
||||
return {
|
||||
left: `${props.position.x}px`,
|
||||
top: `${props.position.y}px`
|
||||
}
|
||||
})
|
||||
|
||||
const hasLocation = computed(() => {
|
||||
return props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0 ||
|
||||
props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0
|
||||
})
|
||||
|
||||
const firstLocation = computed(() => {
|
||||
if (props.event?.data?.locationsPhysical && Object.keys(props.event.data.locationsPhysical).length > 0) {
|
||||
const key = Object.keys(props.event.data.locationsPhysical)[0]
|
||||
return props.event.data.locationsPhysical[key]
|
||||
}
|
||||
if (props.event?.data?.locationsVirtual && Object.keys(props.event.data.locationsVirtual).length > 0) {
|
||||
const key = Object.keys(props.event.data.locationsVirtual)[0]
|
||||
return props.event.data.locationsVirtual[key]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const participantCount = computed(() => {
|
||||
if (!props.event?.data?.participants) return 0
|
||||
return Object.keys(props.event.data.participants).length
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="visible && event"
|
||||
class="event-popup"
|
||||
:style="popupStyle"
|
||||
@click.stop="$emit('click', event)"
|
||||
>
|
||||
<v-card elevation="8" class="event-popup-card">
|
||||
<v-card-text class="pa-3">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-calendar" size="small" class="mr-2" />
|
||||
<div class="text-subtitle-1 font-weight-bold">{{ event.data?.label || 'Untitled Event' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="event-popup-details">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon icon="mdi-clock-outline" size="small" class="mr-2 text-grey" />
|
||||
<div class="text-caption">
|
||||
<div v-if="event.data?.timeless">
|
||||
All Day - {{ new Date(event.data.startsOn).toLocaleDateString() }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ formatTime(event.data?.startsOn) }} - {{ formatTime(event.data?.endsOn) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasLocation" class="d-flex align-center mb-1">
|
||||
<v-icon icon="mdi-map-marker" size="small" class="mr-2 text-grey" />
|
||||
<div class="text-caption text-truncate">
|
||||
{{ firstLocation?.label || firstLocation?.location || 'Location' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="event.data?.organizer" class="d-flex align-center mb-1">
|
||||
<v-icon icon="mdi-account" size="small" class="mr-2 text-grey" />
|
||||
<div class="text-caption">
|
||||
{{ event.data.organizer.name || event.data.organizer.address }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="participantCount > 0" class="d-flex align-center mb-1">
|
||||
<v-icon icon="mdi-account-group" size="small" class="mr-2 text-grey" />
|
||||
<div class="text-caption">
|
||||
{{ participantCount }} participant{{ participantCount !== 1 ? 's' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="event.data?.description" class="mt-2 pt-2 border-t">
|
||||
<div class="text-caption text-grey">{{ event.data.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="event.data?.tags && event.data.tags.length > 0" class="mt-2">
|
||||
<v-chip
|
||||
v-for="(tag, index) in event.data.tags.slice(0, 3)"
|
||||
:key="index"
|
||||
size="x-small"
|
||||
class="mr-1"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
<span v-if="event.data.tags.length > 3" class="text-caption text-grey">
|
||||
+{{ event.data.tags.length - 3 }} more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-lighten-1 mt-2 text-center">
|
||||
Click to view details
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.event-popup {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
pointer-events: auto;
|
||||
transform: translate(10px, 10px);
|
||||
}
|
||||
|
||||
.event-popup-card {
|
||||
min-width: 280px;
|
||||
max-width: 350px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.event-popup-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.event-popup-details {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top: 1px solid rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
/* Fade transition */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user