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>
100 lines
2.8 KiB
Markdown
100 lines
2.8 KiB
Markdown
# Data Model: Event List on Home Page
|
|
|
|
**Feature**: 009-list-events | **Date**: 2026-03-08
|
|
|
|
## Entities
|
|
|
|
### StoredEvent (existing — no changes)
|
|
|
|
The `StoredEvent` interface in `frontend/src/composables/useEventStorage.ts` already contains all fields needed for the event list feature.
|
|
|
|
```typescript
|
|
interface StoredEvent {
|
|
eventToken: string // Required — UUID, used for navigation
|
|
organizerToken?: string // Present if user created this event
|
|
title: string // Required — displayed on card
|
|
dateTime: string // Required — ISO 8601, used for sorting + relative time
|
|
expiryDate: string // Stored but not displayed in list view
|
|
rsvpToken?: string // Present if user RSVP'd to this event
|
|
rsvpName?: string // User's name at RSVP time
|
|
}
|
|
```
|
|
|
|
### Validation Rules
|
|
|
|
An event entry is considered **valid** for display if all of:
|
|
- `eventToken` is a non-empty string
|
|
- `title` is a non-empty string
|
|
- `dateTime` is a non-empty string that parses to a valid `Date`
|
|
|
|
Invalid entries are silently excluded from the list (FR-010).
|
|
|
|
### Derived Properties (computed at render time)
|
|
|
|
| Property | Derivation |
|
|
|----------|-----------|
|
|
| `isPast` | `new Date(dateTime) < new Date()` |
|
|
| `isOrganizer` | `organizerToken !== undefined` |
|
|
| `isAttendee` | `rsvpToken !== undefined && organizerToken === undefined` |
|
|
| `relativeTime` | `Intl.RelativeTimeFormat` applied to `dateTime` vs now |
|
|
| `detailRoute` | `/events/${eventToken}` |
|
|
|
|
### Sorting Order
|
|
|
|
1. **Upcoming events** (`dateTime >= now`): ascending by `dateTime` (soonest first)
|
|
2. **Past events** (`dateTime < now`): descending by `dateTime` (most recently passed first)
|
|
|
|
### Composable Extension
|
|
|
|
The `useEventStorage` composable needs one new function:
|
|
|
|
```typescript
|
|
function removeEvent(eventToken: string): void {
|
|
const events = readEvents().filter((e) => e.eventToken !== eventToken)
|
|
writeEvents(events)
|
|
}
|
|
```
|
|
|
|
Returned alongside existing functions from `useEventStorage()`.
|
|
|
|
## State Transitions
|
|
|
|
```
|
|
localStorage read
|
|
│
|
|
▼
|
|
Parse JSON ──(error)──► empty array
|
|
│
|
|
▼
|
|
Validate entries ──(invalid)──► silently excluded
|
|
│
|
|
▼
|
|
Split: upcoming / past
|
|
│
|
|
▼
|
|
Sort each group
|
|
│
|
|
▼
|
|
Concatenate ──► rendered list
|
|
```
|
|
|
|
### Remove Event Flow
|
|
|
|
```
|
|
User taps delete icon / swipes left
|
|
│
|
|
▼
|
|
ConfirmDialog opens
|
|
│
|
|
┌────┴────┐
|
|
│ Cancel │ Confirm
|
|
│ │ │
|
|
│ ▼ ▼
|
|
│ removeEvent(token)
|
|
│ │
|
|
│ ▼
|
|
│ Event removed from localStorage
|
|
│ List re-renders (event disappears)
|
|
└────────────────────────────────┘
|
|
```
|