226 lines
10 KiB
Vue
226 lines
10 KiB
Vue
<script setup lang="ts">
|
||
import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar'
|
||
import { CircleAlert, Clock, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next';
|
||
import { computed, onMounted, ref, watch } from 'vue';
|
||
import ButtonGroup from '../ui/button-group/ButtonGroup.vue';
|
||
import Button from '../ui/button/Button.vue';
|
||
import { CalendarAttendance, getCalendarEvent, setCalendarEventAttendance, setCancelCalendarEvent } from '@/api/calendar';
|
||
import { useUserStore } from '@/stores/user';
|
||
import { useRoute } from 'vue-router';
|
||
import DropdownMenu from '../ui/dropdown-menu/DropdownMenu.vue';
|
||
import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue';
|
||
import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue';
|
||
import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue';
|
||
|
||
const route = useRoute();
|
||
// const eventID = computed(() => {
|
||
// const id = route.params.id;
|
||
// if (typeof id === 'string') return id;
|
||
// return undefined;
|
||
// });
|
||
|
||
const loaded = ref<boolean>(false);
|
||
const activeEvent = ref<CalendarEvent | null>(null);
|
||
|
||
// onMounted(async () => {
|
||
// let eventID = route.params.id;
|
||
// console.log(eventID);
|
||
// activeEvent.value = await getCalendarEvent(Number(eventID));
|
||
// loaded.value = true;
|
||
// });
|
||
|
||
watch(
|
||
() => route.params.id,
|
||
async (id) => {
|
||
if (!id) return;
|
||
activeEvent.value = await getCalendarEvent(Number(id));
|
||
loaded.value = true;
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'close'): void
|
||
(e: 'reload'): void
|
||
(e: 'edit', event: CalendarEvent): void
|
||
}>()
|
||
|
||
// const activeEvent = computed(() => props.event)
|
||
|
||
const startFmt = new Intl.DateTimeFormat(undefined, {
|
||
weekday: 'short', year: 'numeric', month: 'short', day: 'numeric',
|
||
hour: 'numeric', minute: '2-digit'
|
||
})
|
||
const endFmt = new Intl.DateTimeFormat(undefined, {
|
||
hour: 'numeric', minute: '2-digit'
|
||
})
|
||
|
||
const whenText = computed(() => {
|
||
if (!activeEvent.value?.start) return ''
|
||
const s = new Date(activeEvent.value.start)
|
||
const e = activeEvent.value?.end ? new Date(activeEvent.value.end) : null
|
||
return e
|
||
? `${startFmt.format(s)} – ${endFmt.format(e)}`
|
||
: `${startFmt.format(s)}`
|
||
})
|
||
|
||
const attending = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Attending) })
|
||
const maybe = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Maybe) })
|
||
const declined = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.NotAttending) })
|
||
const viewedState = ref<CalendarAttendance>(CalendarAttendance.Attending);
|
||
|
||
let user = useUserStore();
|
||
const myAttendance = computed<CalendarSignup | null>(() => {
|
||
return activeEvent.value.eventSignups.find(
|
||
(s) => s.member_id === user.user.id
|
||
) || null;
|
||
});
|
||
|
||
async function setAttendance(state: CalendarAttendance) {
|
||
await setCalendarEventAttendance(activeEvent.value.id, state);
|
||
//refresh event data
|
||
activeEvent.value = await getCalendarEvent(activeEvent.value.id);
|
||
}
|
||
|
||
const canEditEvent = computed(() => {
|
||
if (user.user.id == activeEvent.value.creator_id)
|
||
return true;
|
||
});
|
||
|
||
async function setCancel(isCancelled: boolean) {
|
||
await setCancelCalendarEvent(activeEvent.value.id, isCancelled);
|
||
emit("reload");
|
||
activeEvent.value = await getCalendarEvent(activeEvent.value.id);
|
||
}
|
||
|
||
async function forceReload() {
|
||
activeEvent.value = await getCalendarEvent(activeEvent.value.id);
|
||
}
|
||
|
||
defineExpose({forceReload})
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="loaded">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between gap-3 border-b px-4 py-3">
|
||
<h2 class="text-lg font-semibold break-all">
|
||
{{ activeEvent?.name || 'Event' }}
|
||
</h2>
|
||
<div class="flex gap-4">
|
||
<DropdownMenu v-if="canEditEvent">
|
||
<DropdownMenuTrigger>
|
||
<button
|
||
class="inline-flex flex-none size-8 items-center justify-center cursor-pointer rounded-md hover:bg-muted/40 transition">
|
||
<EllipsisVertical class="size-6" />
|
||
</button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent>
|
||
<DropdownMenuItem @click="emit('edit', activeEvent)">
|
||
Edit
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem v-if="activeEvent.cancelled"
|
||
@click="setCancel(false)">
|
||
Un-Cancel
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem v-else @click="setCancel(true)">
|
||
Cancel
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
<button
|
||
class="inline-flex flex-none size-8 items-center justify-center rounded-md border hover:bg-muted/40 transition cursor-pointer"
|
||
aria-label="Close" @click="emit('close')">
|
||
<X class="size-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- Body -->
|
||
<div class="flex-1 flex flex-col items-center min-h-0 overflow-y-auto px-4 py-4 space-y-6 w-full">
|
||
<section v-if="activeEvent.cancelled == true" class="w-full">
|
||
<div class="flex p-2 rounded-md w-full bg-destructive gap-3">
|
||
<CircleAlert></CircleAlert> This event has been cancelled
|
||
</div>
|
||
</section>
|
||
<section class="w-full">
|
||
<ButtonGroup class="flex w-full">
|
||
<Button variant="outline"
|
||
:class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''"
|
||
@click="setAttendance(CalendarAttendance.Attending)">Going</Button>
|
||
<Button variant="outline"
|
||
:class="myAttendance?.status === CalendarAttendance.Maybe ? 'border-2 !border-l-2 border-primary text-primary' : ''"
|
||
@click="setAttendance(CalendarAttendance.Maybe)">Maybe</Button>
|
||
<Button variant="outline"
|
||
:class="myAttendance?.status === CalendarAttendance.NotAttending ? 'border-2 !border-l-2 border-primary text-primary' : ''"
|
||
@click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button>
|
||
</ButtonGroup>
|
||
</section>
|
||
<!-- When -->
|
||
<section v-if="whenText" class="space-y-2 w-full">
|
||
<div class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm">
|
||
<Clock class="size-4 opacity-80" />
|
||
<span class="font-medium">{{ whenText }}</span>
|
||
</div>
|
||
</section>
|
||
<!-- Quick meta chips -->
|
||
<section class="flex flex-wrap gap-2 w-full">
|
||
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
|
||
<MapPin class="size-3.5 opacity-80" />
|
||
<span class="font-medium">{{ activeEvent.location || "Unknown" }}</span>
|
||
</span>
|
||
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
|
||
<User class="size-3.5 opacity-80" />
|
||
<span class="font-medium">Created by: {{ activeEvent.creator_name || "Unknown User"
|
||
}}</span>
|
||
</span>
|
||
</section>
|
||
<!-- Description -->
|
||
<section class="space-y-2 w-full">
|
||
<p class="text-lg font-semibold">Description</p>
|
||
<p class="border bg-muted/50 px-3 py-2 rounded-lg min-h-24 my-2 whitespace-pre-line">
|
||
{{ activeEvent.description }}
|
||
</p>
|
||
</section>
|
||
<!-- Attendance -->
|
||
<section class="space-y-2 w-full">
|
||
<p class="text-lg font-semibold">Attendance</p>
|
||
<div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2">
|
||
<div class="flex w-full pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer">
|
||
<label
|
||
:class="viewedState === CalendarAttendance.Attending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
||
@click="viewedState = CalendarAttendance.Attending">Going {{ attending.length }}</label>
|
||
<label
|
||
:class="viewedState === CalendarAttendance.Maybe ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
||
@click="viewedState = CalendarAttendance.Maybe">Maybe {{ maybe.length }}</label>
|
||
<label
|
||
:class="viewedState === CalendarAttendance.NotAttending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
||
@click="viewedState = CalendarAttendance.NotAttending">Declined {{ declined.length
|
||
}}</label>
|
||
</div>
|
||
<div class="px-5 py-4 min-h-28">
|
||
<div v-if="viewedState === CalendarAttendance.Attending" v-for="person in attending">
|
||
<p>{{ person.member_name }}</p>
|
||
</div>
|
||
<div v-if="viewedState === CalendarAttendance.Maybe" v-for="person in maybe">
|
||
<p>{{ person.member_name }}</p>
|
||
</div>
|
||
<div v-if="viewedState === CalendarAttendance.NotAttending" v-for="person in declined">
|
||
<p>{{ person.member_name }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<!-- Footer (optional actions) -->
|
||
<!-- <div class="border-t px-4 py-3 flex items-center justify-end gap-2">
|
||
<button class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40 transition">
|
||
Edit
|
||
</button>
|
||
<button
|
||
class="rounded-md bg-primary text-primary-foreground px-3 py-1.5 text-sm hover:opacity-90 transition">
|
||
Open details
|
||
</button>
|
||
</div> -->
|
||
|
||
</div>
|
||
</template> |