Separate event dipslay from event slot display
Pull out the list of events into its own page sorted by name and show the event slots in chronological order on the schedule page, with past slots hidden by default. This makes the content underneath the schedule the most immediately useful to have in the moment, while the full list is kept separately and in a predictable order.
This commit is contained in:
parent
848a330f3a
commit
ae1c653af6
5 changed files with 198 additions and 5 deletions
68
components/CardEventSlot.vue
Normal file
68
components/CardEventSlot.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<section class="eventSlot">
|
||||
<hgroup>
|
||||
<h3>{{ event?.name }}</h3>
|
||||
<p>
|
||||
{{ formatTime(slot.start) }} - {{ formatTime(slot.end) }}
|
||||
</p>
|
||||
</hgroup>
|
||||
<p>{{ event?.description ?? "No description provided" }}</p>
|
||||
<p v-if="locations.length">
|
||||
At {{ locations.map(location => location?.name ?? "unknown").join(" + ") }}
|
||||
</p>
|
||||
<p v-if="event?.interested">
|
||||
{{ event?.interested }}
|
||||
<template v-if="slot.interested">
|
||||
+ {{ slot.interested }}
|
||||
</template>
|
||||
interested
|
||||
</p>
|
||||
<p v-if="slot.assigned.size">
|
||||
Crew:
|
||||
{{ [...slot.assigned].map(id => usersStore.users.get(id)?.name).join(", ") }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DateTime } from '~/shared/utils/luxon';
|
||||
|
||||
const props = defineProps<{
|
||||
event?: ClientScheduleEvent,
|
||||
slot: ClientScheduleEventSlot,
|
||||
}>()
|
||||
const scheduleStore = useSchedulesStore();
|
||||
const schedule = scheduleStore.activeSchedule;
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const locations = computed(() => [...props.slot.locationIds].map(id => schedule.value.locations.get(id)));
|
||||
|
||||
function formatTime(time: DateTime) {
|
||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.eventSlot {
|
||||
background: color-mix(in oklab, var(--background), grey 20%);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.eventSlot h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.eventSlot + .eventSlot {
|
||||
margin-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding-inline: 0.2em;
|
||||
}
|
||||
button.active {
|
||||
color: color-mix(in oklab, var(--foreground), green 50%);
|
||||
}
|
||||
</style>
|
|
@ -9,6 +9,9 @@
|
|||
<li>
|
||||
<NuxtLink to="/">Home</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/events">Events</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/schedule">Schedule</NuxtLink>
|
||||
</li>
|
||||
|
|
89
pages/events.vue
Normal file
89
pages/events.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<main>
|
||||
<h1>Events</h1>
|
||||
<label v-if="accountStore.valid">
|
||||
Filter:
|
||||
<select
|
||||
v-model="filter"
|
||||
>
|
||||
<option
|
||||
:value="undefined"
|
||||
:selected="filter === undefined"
|
||||
><All events></option>
|
||||
<option
|
||||
value="my-schedule"
|
||||
:selected='filter === "my-schedule"'
|
||||
>My Schedule</option>
|
||||
<option
|
||||
v-if="accountStore.isCrew"
|
||||
value="assigned"
|
||||
:selected='filter === "assigned"'
|
||||
>Assigned to Me</option>
|
||||
</select>
|
||||
</label>
|
||||
<CardEvent
|
||||
v-for="event in events"
|
||||
:key="`my-${event.id}`"
|
||||
:id="String(event.name)"
|
||||
:event
|
||||
></CardEvent>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: "Schedule",
|
||||
});
|
||||
|
||||
const stringSort = useStringSort();
|
||||
const events = computed(() => {
|
||||
return [...schedule.value.events.values()].filter(e => !e.deleted && [...e.slots.values()].some(eventSlotFilter.value)).sort((a, b) => stringSort(a.name, b.name));
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const usersStore = useUsersStore();
|
||||
await usersStore.fetch();
|
||||
const schedule = await useSchedule();
|
||||
|
||||
const route = useRoute();
|
||||
const filter = computed({
|
||||
get: () => queryToString(route.query.filter),
|
||||
set: (value: string | undefined) => navigateTo({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
filter: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const eventSlotFilter = computed(() => {
|
||||
if (filter.value === undefined || !accountStore.valid || schedule.value.deleted) {
|
||||
return () => true;
|
||||
}
|
||||
const aid = accountStore.id;
|
||||
if (filter.value === "my-schedule") {
|
||||
const slotIds = new Set(accountStore.interestedEventSlotIds);
|
||||
for (const event of schedule.value.events.values()) {
|
||||
if (!event.deleted && accountStore.interestedEventIds.has(event.id)) {
|
||||
for (const slot of event.slots.values()) {
|
||||
slotIds.add(slot.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (slot: ClientScheduleEventSlot) => slotIds.has(slot.id) || slot.assigned.has(aid!) || false;
|
||||
}
|
||||
if (filter.value === "assigned") {
|
||||
return (slot: ClientScheduleEventSlot) => slot.assigned.has(aid!) || false;
|
||||
}
|
||||
if (filter.value.startsWith("crew-")) {
|
||||
const cid = parseInt(filter.value.slice(5));
|
||||
return (slot: ClientScheduleEventSlot) => slot.assigned.has(cid) || false;
|
||||
}
|
||||
return () => false;
|
||||
});
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
-->
|
||||
<template>
|
||||
<main>
|
||||
<h1>Schedule & Events</h1>
|
||||
<h1>Schedule</h1>
|
||||
<p>
|
||||
Study carefully, we only hold these events once a year.
|
||||
</p>
|
||||
|
@ -46,10 +46,15 @@
|
|||
</label>
|
||||
<Timetable :schedule :eventSlotFilter :shiftSlotFilter />
|
||||
<h2>Events</h2>
|
||||
<CardEvent
|
||||
v-for="event in [...schedule.events.values()].filter(e => !e.deleted && [...e.slots.values()].some(eventSlotFilter))"
|
||||
:key="event.id"
|
||||
:event
|
||||
<label>
|
||||
Hide past events
|
||||
<input type="checkbox" v-model="hidePastEvents">
|
||||
</label>
|
||||
<CardEventSlot
|
||||
v-for="eventSlot in eventSlots"
|
||||
:key="eventSlot.slot.id"
|
||||
:event="eventSlot.event"
|
||||
:slot="eventSlot.slot"
|
||||
/>
|
||||
<template v-if="accountStore.isCrew">
|
||||
<h2>Shifts</h2>
|
||||
|
@ -91,6 +96,17 @@ const filter = computed({
|
|||
}),
|
||||
});
|
||||
|
||||
const hidePastEvents = computed({
|
||||
get: () => !queryToBoolean(route.query.showPast),
|
||||
set: (value: boolean) => navigateTo({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
showPast: value ? undefined : null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const eventSlotFilter = computed(() => {
|
||||
if (filter.value === undefined || !accountStore.valid || schedule.value.deleted) {
|
||||
return () => true;
|
||||
|
@ -130,4 +146,15 @@ const shiftSlotFilter = computed(() => {
|
|||
}
|
||||
return () => false;
|
||||
});
|
||||
|
||||
const eventSlots = computed(() => {
|
||||
let slots = [...schedule.value.eventSlots.values()].filter(slot => !slot.deleted && eventSlotFilter.value(slot));
|
||||
if (hidePastEvents.value) {
|
||||
const nowMs = Date.now();
|
||||
slots = slots.filter(slot => slot.end.toMillis() >= nowMs);
|
||||
}
|
||||
slots.sort((a, b) => a.start.toMillis() - b.start.toMillis() || a.end.toMillis() - b.end.toMillis());
|
||||
return slots.map(slot => ({ slot, event: schedule.value.events.get(slot.eventId!) }));
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -21,6 +21,12 @@ export function queryToNumber(item?: null | LocationQueryValue | LocationQueryVa
|
|||
return Number.parseInt(item, 10);
|
||||
}
|
||||
|
||||
export function queryToBoolean(item?: null | LocationQueryValue | LocationQueryValue[]) {
|
||||
if (item === undefined)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function idMap<T extends { id: Id }>(entities: T[]) {
|
||||
return new Map(entities.map(entity => [entity.id, entity]));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue