Add event list feature (009-list-events)
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>
This commit is contained in:
99
specs/009-list-events/data-model.md
Normal file
99
specs/009-list-events/data-model.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 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)
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
Reference in New Issue
Block a user