Organizers can now cancel events directly from the event list via the
existing PATCH /events/{eventToken} API. The confirmation dialog shows
role-differentiated messaging: "Cancel event?" with a severity warning
for organizers vs. "Remove event?" for attendees. Responses 204, 409,
and 404 all result in successful removal from the local list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
177 lines
5.1 KiB
Vue
177 lines
5.1 KiB
Vue
<template>
|
|
<div class="event-list">
|
|
<section
|
|
v-for="section in groupedSections"
|
|
:key="section.key"
|
|
:aria-label="section.label"
|
|
class="event-section"
|
|
>
|
|
<SectionHeader :label="section.label" :emphasized="section.emphasized" />
|
|
<div role="list">
|
|
<template v-for="group in section.dateGroups" :key="group.dateKey">
|
|
<DateSubheader v-if="group.showSubheader" :label="group.label" />
|
|
<div v-for="event in group.events" :key="event.eventToken" role="listitem">
|
|
<EventCard
|
|
:event-token="event.eventToken"
|
|
:title="event.title"
|
|
:relative-time="formatRelativeTime(event.dateTime)"
|
|
:is-past="section.key === 'past'"
|
|
:event-role="getRole(event)"
|
|
:time-display-mode="section.key === 'past' ? 'relative' : 'clock'"
|
|
:date-time="event.dateTime"
|
|
@delete="requestDelete"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</section>
|
|
<ConfirmDialog
|
|
:open="!!pendingDeleteToken"
|
|
:title="deleteDialogTitle"
|
|
:message="deleteDialogMessage"
|
|
confirm-label="Remove"
|
|
cancel-label="Cancel"
|
|
@confirm="confirmDelete"
|
|
@cancel="cancelDelete"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useEventStorage, isValidStoredEvent } from '../composables/useEventStorage'
|
|
import { useEventGrouping } from '../composables/useEventGrouping'
|
|
import { formatRelativeTime } from '../composables/useRelativeTime'
|
|
import { api } from '../api/client'
|
|
import EventCard from './EventCard.vue'
|
|
import SectionHeader from './SectionHeader.vue'
|
|
import DateSubheader from './DateSubheader.vue'
|
|
import ConfirmDialog from './ConfirmDialog.vue'
|
|
import type { StoredEvent } from '../composables/useEventStorage'
|
|
|
|
const { getStoredEvents, getRsvp, getOrganizerToken, removeEvent } = useEventStorage()
|
|
|
|
const pendingDeleteToken = ref<string | null>(null)
|
|
const deleteError = ref('')
|
|
|
|
const pendingDeleteRole = computed(() => {
|
|
if (!pendingDeleteToken.value) return null
|
|
const event = getStoredEvents().find((e) => e.eventToken === pendingDeleteToken.value)
|
|
return event ? getRole(event) : null
|
|
})
|
|
|
|
const deleteDialogTitle = computed(() => {
|
|
return pendingDeleteRole.value === 'organizer' ? 'Cancel event?' : 'Remove event?'
|
|
})
|
|
|
|
const deleteDialogMessage = computed(() => {
|
|
if (!pendingDeleteToken.value) return ''
|
|
if (pendingDeleteRole.value === 'organizer') {
|
|
return 'This will permanently cancel the event for all attendees.'
|
|
}
|
|
const rsvp = getRsvp(pendingDeleteToken.value)
|
|
if (rsvp) {
|
|
return 'This event will be removed from your list and your attendance will be cancelled.'
|
|
}
|
|
return 'This event will be removed from your list.'
|
|
})
|
|
|
|
function requestDelete(eventToken: string) {
|
|
deleteError.value = ''
|
|
const role = getRole(getStoredEvents().find((e) => e.eventToken === eventToken)!)
|
|
if (role === 'watcher') {
|
|
removeEvent(eventToken)
|
|
return
|
|
}
|
|
pendingDeleteToken.value = eventToken
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
if (!pendingDeleteToken.value) return
|
|
|
|
const eventToken = pendingDeleteToken.value
|
|
const organizerToken = getOrganizerToken(eventToken)
|
|
|
|
if (organizerToken) {
|
|
try {
|
|
const { response } = await api.PATCH('/events/{eventToken}', {
|
|
params: {
|
|
path: { eventToken },
|
|
query: { organizerToken },
|
|
},
|
|
body: { cancelled: true },
|
|
})
|
|
|
|
if (response.status !== 204 && response.status !== 409 && response.status !== 404) {
|
|
deleteError.value = 'Could not cancel event. Please try again.'
|
|
return
|
|
}
|
|
} catch {
|
|
deleteError.value = 'Could not cancel event. Please try again.'
|
|
return
|
|
}
|
|
|
|
removeEvent(eventToken)
|
|
pendingDeleteToken.value = null
|
|
return
|
|
}
|
|
|
|
const rsvp = getRsvp(eventToken)
|
|
|
|
if (rsvp) {
|
|
try {
|
|
const { response } = await api.DELETE('/events/{eventToken}/rsvps/{rsvpToken}', {
|
|
params: {
|
|
path: {
|
|
eventToken: eventToken,
|
|
rsvpToken: rsvp.rsvpToken,
|
|
},
|
|
},
|
|
})
|
|
|
|
if (response.status !== 204 && response.status !== 404) {
|
|
deleteError.value = 'Could not cancel attendance. Please try again.'
|
|
pendingDeleteToken.value = null
|
|
return
|
|
}
|
|
} catch {
|
|
deleteError.value = 'Could not cancel attendance. Please try again.'
|
|
pendingDeleteToken.value = null
|
|
return
|
|
}
|
|
}
|
|
|
|
removeEvent(eventToken)
|
|
pendingDeleteToken.value = null
|
|
}
|
|
|
|
function cancelDelete() {
|
|
pendingDeleteToken.value = null
|
|
}
|
|
|
|
function getRole(event: StoredEvent): 'organizer' | 'attendee' | 'watcher' {
|
|
if (event.organizerToken) return 'organizer'
|
|
if (event.rsvpToken) return 'attendee'
|
|
return 'watcher'
|
|
}
|
|
|
|
const groupedSections = computed(() => {
|
|
const valid = getStoredEvents().filter(isValidStoredEvent)
|
|
return useEventGrouping(valid)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.event-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.event-section [role="list"] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
</style>
|