Enable users to see all their saved events on the home screen, sorted by date with upcoming events first. Key capabilities: - EventCard with title, relative time display, and organizer/attendee role badge - Sortable EventList with past-event visual distinction (faded style) - Empty state when no events are stored - Swipe-to-delete gesture with confirmation dialog - Floating action button for quick event creation - Rename router param :token → :eventToken across all views - useRelativeTime composable (Intl.RelativeTimeFormat) - useEventStorage: add validation, removeEvent(), reactive versioning - Full E2E and unit test coverage for all new components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.3 KiB
Vue
80 lines
2.3 KiB
Vue
<template>
|
|
<div class="event-list" role="list" aria-label="Your events">
|
|
<div v-for="event in sortedEvents" :key="event.eventToken" role="listitem">
|
|
<EventCard
|
|
:event-token="event.eventToken"
|
|
:title="event.title"
|
|
:relative-time="formatRelativeTime(event.dateTime)"
|
|
:is-past="isPast(event.dateTime)"
|
|
:event-role="getRole(event)"
|
|
@delete="requestDelete"
|
|
/>
|
|
</div>
|
|
<ConfirmDialog
|
|
:open="!!pendingDeleteToken"
|
|
title="Remove event?"
|
|
message="This event will be removed from your list."
|
|
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 { formatRelativeTime } from '../composables/useRelativeTime'
|
|
import EventCard from './EventCard.vue'
|
|
import ConfirmDialog from './ConfirmDialog.vue'
|
|
import type { StoredEvent } from '../composables/useEventStorage'
|
|
|
|
const { getStoredEvents, removeEvent } = useEventStorage()
|
|
|
|
const pendingDeleteToken = ref<string | null>(null)
|
|
|
|
function requestDelete(eventToken: string) {
|
|
pendingDeleteToken.value = eventToken
|
|
}
|
|
|
|
function confirmDelete() {
|
|
if (pendingDeleteToken.value) {
|
|
removeEvent(pendingDeleteToken.value)
|
|
}
|
|
pendingDeleteToken.value = null
|
|
}
|
|
|
|
function cancelDelete() {
|
|
pendingDeleteToken.value = null
|
|
}
|
|
|
|
function isPast(dateTime: string): boolean {
|
|
return new Date(dateTime) < new Date()
|
|
}
|
|
|
|
function getRole(event: StoredEvent): 'organizer' | 'attendee' | undefined {
|
|
if (event.organizerToken) return 'organizer'
|
|
if (event.rsvpToken) return 'attendee'
|
|
return undefined
|
|
}
|
|
|
|
const sortedEvents = computed(() => {
|
|
const valid = getStoredEvents().filter(isValidStoredEvent)
|
|
const now = new Date()
|
|
const upcoming = valid.filter((e) => new Date(e.dateTime) >= now)
|
|
const past = valid.filter((e) => new Date(e.dateTime) < now)
|
|
upcoming.sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime())
|
|
past.sort((a, b) => new Date(b.dateTime).getTime() - new Date(a.dateTime).getTime())
|
|
return [...upcoming, ...past]
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.event-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
</style>
|