Add cancel RSVP feature (backend DELETE endpoint + frontend UI)
Allows guests to cancel their RSVP via a DELETE endpoint using their guestToken. Frontend shows cancel button in RsvpBar and clears local storage on success. Includes unit tests, integration tests, and E2E spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,11 +70,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel error message -->
|
||||
<div v-if="cancelError" class="detail__cancel-error" role="alert">
|
||||
<p>{{ cancelError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- RSVP bar -->
|
||||
<RsvpBar
|
||||
v-if="state === 'loaded' && event && !isOrganizer"
|
||||
:has-rsvp="!!rsvpName"
|
||||
@open="sheetOpen = true"
|
||||
@cancel="confirmCancelOpen = true"
|
||||
/>
|
||||
|
||||
<!-- Cancel confirmation dialog -->
|
||||
<ConfirmDialog
|
||||
:open="confirmCancelOpen"
|
||||
title="Cancel attendance?"
|
||||
message="Your attendance will be permanently cancelled."
|
||||
confirm-label="Cancel attendance"
|
||||
cancel-label="Keep"
|
||||
@confirm="handleCancelRsvp"
|
||||
@cancel="confirmCancelOpen = false"
|
||||
/>
|
||||
|
||||
<!-- RSVP bottom sheet -->
|
||||
@@ -113,6 +130,7 @@ import { api } from '@/api/client'
|
||||
import { useEventStorage } from '@/composables/useEventStorage'
|
||||
import AttendeeList from '@/components/AttendeeList.vue'
|
||||
import BottomSheet from '@/components/BottomSheet.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import RsvpBar from '@/components/RsvpBar.vue'
|
||||
import type { components } from '@/api/schema'
|
||||
|
||||
@@ -120,7 +138,7 @@ type GetEventResponse = components['schemas']['GetEventResponse']
|
||||
type State = 'loading' | 'loaded' | 'not-found' | 'error'
|
||||
|
||||
const route = useRoute()
|
||||
const { saveRsvp, getRsvp, getOrganizerToken } = useEventStorage()
|
||||
const { saveRsvp, getRsvp, removeRsvp, getOrganizerToken } = useEventStorage()
|
||||
|
||||
const state = ref<State>('loading')
|
||||
const event = ref<GetEventResponse | null>(null)
|
||||
@@ -132,6 +150,8 @@ const nameError = ref('')
|
||||
const submitError = ref('')
|
||||
const submitting = ref(false)
|
||||
const rsvpName = ref<string | undefined>(undefined)
|
||||
const confirmCancelOpen = ref(false)
|
||||
const cancelError = ref('')
|
||||
const isOrganizer = ref(false)
|
||||
const attendeeNames = ref<string[] | null>(null)
|
||||
|
||||
@@ -228,6 +248,37 @@ async function submitRsvp() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancelRsvp() {
|
||||
confirmCancelOpen.value = false
|
||||
cancelError.value = ''
|
||||
|
||||
const stored = getRsvp(route.params.eventToken as string)
|
||||
if (!stored) return
|
||||
|
||||
try {
|
||||
const { response } = await api.DELETE('/events/{token}/rsvps/{rsvpToken}', {
|
||||
params: {
|
||||
path: {
|
||||
token: route.params.eventToken as string,
|
||||
rsvpToken: stored.rsvpToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 204 || response.status === 404) {
|
||||
removeRsvp(route.params.eventToken as string)
|
||||
rsvpName.value = undefined
|
||||
if (event.value) {
|
||||
event.value.attendeeCount = Math.max(0, event.value.attendeeCount - 1)
|
||||
}
|
||||
} else {
|
||||
cancelError.value = 'Could not cancel RSVP. Please try again.'
|
||||
}
|
||||
} catch {
|
||||
cancelError.value = 'Could not cancel RSVP. Please try again.'
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAttendees(eventToken: string, organizerToken: string) {
|
||||
try {
|
||||
const { data, error } = await api.GET('/events/{token}/attendees', {
|
||||
|
||||
Reference in New Issue
Block a user