- + Create Event
+
+
+
+
+
+
+
diff --git a/frontend/src/views/__tests__/EventCreateView.spec.ts b/frontend/src/views/__tests__/EventCreateView.spec.ts
index 441a2a1..ee08832 100644
--- a/frontend/src/views/__tests__/EventCreateView.spec.ts
+++ b/frontend/src/views/__tests__/EventCreateView.spec.ts
@@ -25,7 +25,7 @@ function createTestRouter() {
routes: [
{ path: '/', name: 'home', component: { template: '' } },
{ path: '/create', name: 'create-event', component: EventCreateView },
- { path: '/events/:token', name: 'event', component: { template: '' } },
+ { path: '/events/:eventToken', name: 'event', component: { template: '' } },
],
})
}
@@ -169,6 +169,7 @@ describe('EventCreateView', () => {
getOrganizerToken: vi.fn(),
saveRsvp: vi.fn(),
getRsvp: vi.fn(),
+ removeEvent: vi.fn(),
})
vi.mocked(api.POST).mockResolvedValueOnce({
@@ -221,7 +222,7 @@ describe('EventCreateView', () => {
expect(pushSpy).toHaveBeenCalledWith({
name: 'event',
- params: { token: 'abc-123' },
+ params: { eventToken: 'abc-123' },
})
})
diff --git a/frontend/src/views/__tests__/EventDetailView.spec.ts b/frontend/src/views/__tests__/EventDetailView.spec.ts
index 2a1f30e..653a6aa 100644
--- a/frontend/src/views/__tests__/EventDetailView.spec.ts
+++ b/frontend/src/views/__tests__/EventDetailView.spec.ts
@@ -22,6 +22,7 @@ vi.mock('@/composables/useEventStorage', () => ({
getOrganizerToken: mockGetOrganizerToken,
saveRsvp: mockSaveRsvp,
getRsvp: mockGetRsvp,
+ removeEvent: vi.fn(),
})),
}))
@@ -30,7 +31,7 @@ function createTestRouter(_token?: string) {
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '' } },
- { path: '/events/:token', name: 'event', component: EventDetailView },
+ { path: '/events/:eventToken', name: 'event', component: EventDetailView },
],
})
}
diff --git a/frontend/src/views/__tests__/EventStubView.spec.ts b/frontend/src/views/__tests__/EventStubView.spec.ts
index 5f93e1f..528cc59 100644
--- a/frontend/src/views/__tests__/EventStubView.spec.ts
+++ b/frontend/src/views/__tests__/EventStubView.spec.ts
@@ -8,7 +8,7 @@ function createTestRouter() {
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '' } },
- { path: '/events/:token', name: 'event', component: EventStubView },
+ { path: '/events/:eventToken', name: 'event', component: EventStubView },
],
})
}
diff --git a/specs/009-list-events/checklists/requirements.md b/specs/009-list-events/checklists/requirements.md
new file mode 100644
index 0000000..3b8c288
--- /dev/null
+++ b/specs/009-list-events/checklists/requirements.md
@@ -0,0 +1,35 @@
+# Specification Quality Checklist: Event List on Home Page
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-03-08
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
+- Assumptions section documents that no backend changes are needed — this is a frontend-only feature using existing localStorage data.
diff --git a/specs/009-list-events/data-model.md b/specs/009-list-events/data-model.md
new file mode 100644
index 0000000..b62a3d9
--- /dev/null
+++ b/specs/009-list-events/data-model.md
@@ -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)
+ └────────────────────────────────┘
+```
diff --git a/specs/009-list-events/plan.md b/specs/009-list-events/plan.md
new file mode 100644
index 0000000..0497299
--- /dev/null
+++ b/specs/009-list-events/plan.md
@@ -0,0 +1,86 @@
+# Implementation Plan: Event List on Home Page
+
+**Branch**: `009-list-events` | **Date**: 2026-03-08 | **Spec**: `specs/009-list-events/spec.md`
+**Input**: Feature specification from `/specs/009-list-events/spec.md`
+
+## Summary
+
+Transform the home page from a static empty-state placeholder into a dynamic event list that shows all events stored in the browser's localStorage. Each event card displays title, relative time, and role indicator (organizer/attendee). Events are sorted chronologically (upcoming first), past events appear faded, and users can remove events via delete icon or swipe gesture. A FAB provides persistent access to event creation.
+
+This is a **frontend-only** feature — no backend or API changes required. The existing `useEventStorage` composable already provides all necessary data access.
+
+## Technical Context
+
+**Language/Version**: TypeScript 5.9, Vue 3.5
+**Primary Dependencies**: Vue 3, Vue Router 5, Vite
+**Storage**: Browser localStorage via `useEventStorage` composable
+**Testing**: Vitest (unit), Playwright + MSW (E2E)
+**Target Platform**: Mobile-first PWA (centered 480px column on desktop)
+**Project Type**: Web application (frontend-only changes)
+**Performance Goals**: Event list renders within 1 second (SC-001) — trivial given localStorage read
+**Constraints**: No external dependencies, no tracking, WCAG AA, keyboard navigable
+**Scale/Scope**: Typically <50 events in localStorage; no pagination needed
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+| Principle | Status | Notes |
+|-----------|--------|-------|
+| I. Privacy by Design | ✅ PASS | Purely client-side. No data leaves the browser. No analytics. |
+| II. Test-Driven Methodology | ✅ PASS | Unit tests for composable, E2E for each user story. TDD enforced. |
+| III. API-First Development | ✅ N/A | No API changes — this feature reads only from localStorage. |
+| IV. Simplicity & Quality | ✅ PASS | Minimal approach: extend existing composable + new components. No over-engineering. |
+| V. Dependency Discipline | ✅ PASS | No new dependencies. Swipe gesture implemented with native Touch API. Relative time via built-in `Intl.RelativeTimeFormat`. |
+| VI. Accessibility | ✅ PASS | Semantic list markup, ARIA labels, keyboard navigation, WCAG AA contrast on faded past events. |
+
+**Gate result: PASS** — no violations.
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/009-list-events/
+├── plan.md # This file
+├── spec.md # Feature specification
+├── research.md # Phase 0 output
+├── data-model.md # Phase 1 output
+└── tasks.md # Phase 2 output (/speckit.tasks command)
+```
+
+### Source Code (repository root)
+
+```text
+frontend/
+├── src/
+│ ├── composables/
+│ │ ├── useEventStorage.ts # MODIFY: add removeEvent()
+│ │ ├── useRelativeTime.ts # NEW: Intl.RelativeTimeFormat wrapper
+│ │ └── __tests__/
+│ │ ├── useEventStorage.spec.ts # MODIFY: add removeEvent tests
+│ │ └── useRelativeTime.spec.ts # NEW: relative time formatting tests
+│ ├── components/
+│ │ ├── EventCard.vue # NEW: individual event list item
+│ │ ├── EventList.vue # NEW: sorted event list container
+│ │ ├── EmptyState.vue # NEW: extracted empty state
+│ │ ├── CreateEventFab.vue # NEW: floating action button
+│ │ ├── ConfirmDialog.vue # NEW: reusable confirmation prompt
+│ │ └── __tests__/
+│ │ ├── EventCard.spec.ts # NEW
+│ │ ├── EventList.spec.ts # NEW
+│ │ ├── EmptyState.spec.ts # NEW
+│ │ └── ConfirmDialog.spec.ts # NEW
+│ ├── views/
+│ │ └── HomeView.vue # MODIFY: compose list/empty/fab
+│ └── assets/
+│ └── main.css # MODIFY: add event card, faded, fab styles
+└── e2e/
+ └── home-events.spec.ts # NEW: E2E tests for all user stories
+```
+
+**Structure Decision**: Frontend-only changes. New components in `components/`, composable extensions in `composables/`, styles in existing `main.css`. No backend changes.
+
+## Complexity Tracking
+
+No constitution violations — this section is intentionally empty.
diff --git a/specs/009-list-events/research.md b/specs/009-list-events/research.md
new file mode 100644
index 0000000..1877a24
--- /dev/null
+++ b/specs/009-list-events/research.md
@@ -0,0 +1,110 @@
+# Research: Event List on Home Page
+
+**Feature**: 009-list-events | **Date**: 2026-03-08
+
+## Research Questions
+
+### 1. Relative Time Formatting with `Intl.RelativeTimeFormat`
+
+**Decision**: Use the built-in `Intl.RelativeTimeFormat` API directly — no library needed.
+
+**Rationale**: The API is supported in all modern browsers (97%+ coverage). It handles locale-aware output natively (e.g., "in 3 days", "vor 2 Tagen" for German). The spec requires exactly this (FR-002).
+
+**Implementation approach**: Create a `useRelativeTime` composable that:
+1. Takes a date string (ISO 8601) and computes the difference from `now`
+2. Selects the appropriate unit (seconds → minutes → hours → days → weeks → months → years)
+3. Returns a formatted string via `Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' })`
+4. Exposes a reactive `label` that updates (optional — can be static since the list re-reads on mount)
+
+**Alternatives considered**:
+- `date-fns/formatDistance`: Would add a dependency for something the platform already does. Rejected per Principle V.
+- `dayjs/relativeTime`: Same reasoning — unnecessary dependency.
+
+### 2. Swipe-to-Delete Gesture (FR-006b)
+
+**Decision**: Implement with native Touch API (`touchstart`, `touchmove`, `touchend`) — no gesture library.
+
+**Rationale**: The gesture is simple (horizontal swipe on a single element). A library like Hammer.js or @vueuse/gesture would be overkill for one swipe direction on one component type. Per Principle V, dependencies must provide substantial value.
+
+**Implementation approach**:
+1. Track `touchstart` X position on the event card
+2. On `touchmove`, calculate delta-X; if leftward and exceeds threshold (~80px), reveal delete action
+3. On `touchend`, either snap back or trigger confirmation
+4. CSS `transform: translateX()` with `transition` for smooth animation
+5. Desktop users use the visible delete icon (no swipe needed)
+
+**Alternatives considered**:
+- `@vueuse/gesture`: Wraps Hammer.js, adds ~15KB. Rejected — too heavy for one gesture.
+- CSS `scroll-snap` trick: Clever but brittle and poor accessibility. Rejected.
+
+### 3. Past Event Visual Fading (FR-009)
+
+**Decision**: Use CSS `opacity` reduction + `filter: saturate()` for faded appearance.
+
+**Rationale**: The spec says "subtle reduction in contrast and saturation" — not a blunt grey-out. Combining `opacity: 0.6` with `filter: saturate(0.5)` achieves this while keeping text readable. Must verify WCAG AA contrast on the faded state.
+
+**Implementation approach**:
+- Add a `.event-card--past` modifier class
+- Apply `opacity: 0.55; filter: saturate(0.4)` (tune exact values for WCAG AA)
+- Keep `pointer-events: auto` and normal hover/focus styles so the card remains interactive
+- The card still navigates to the event detail page on click
+
+**Contrast verification**: The card text (`#1C1C1E` on `#FFFFFF`) has a contrast ratio of ~17:1. At `opacity: 0.55`, effective contrast drops to ~9:1, which still passes WCAG AA (4.5:1 minimum). Safe.
+
+### 4. Confirmation Dialog (FR-007)
+
+**Decision**: Custom modal component (reusing the existing `BottomSheet.vue` pattern) rather than `window.confirm()`.
+
+**Rationale**: `window.confirm()` is blocking, non-stylable, and inconsistent across browsers. A custom dialog matches the app's design system and provides a better UX. The existing `BottomSheet.vue` already handles teleportation, focus trapping, and Escape-key dismissal — the confirm dialog can reuse this or follow the same pattern.
+
+**Implementation approach**:
+- Create a `ConfirmDialog.vue` component
+- Props: `open`, `title`, `message`, `confirmLabel`, `cancelLabel`
+- Emits: `confirm`, `cancel`
+- Uses the same teleport-to-body pattern as `BottomSheet.vue`
+- Focus trapping and keyboard navigation (Tab, Escape, Enter)
+
+### 5. localStorage Validation (FR-010)
+
+**Decision**: Validate entries during read — filter out invalid events silently.
+
+**Rationale**: The spec says "silently excluded from the list." The `readEvents()` function already handles parse errors with a try/catch. We need to add field-level validation: an event is valid only if it has `eventToken`, `title`, and `dateTime` (all non-empty strings).
+
+**Implementation approach**:
+- Add a `isValidStoredEvent(e: unknown): e is StoredEvent` type guard
+- Apply it in `getStoredEvents()` as a filter
+- Invalid entries remain in localStorage (no destructive cleanup) but are not displayed
+
+### 6. FAB Placement (FR-011)
+
+**Decision**: Fixed-position button at bottom-right with safe-area padding.
+
+**Rationale**: Standard Material Design pattern for primary actions. The existing `RsvpBar.vue` already uses `padding-bottom: env(safe-area-inset-bottom)` for mobile notch avoidance — reuse the same approach.
+
+**Implementation approach**:
+- `position: fixed; bottom: calc(1.2rem + env(safe-area-inset-bottom)); right: 1.2rem`
+- Circular button with `+` icon, accent color background
+- `z-index` above content, shadow for elevation
+- Navigates to `/create` on click
+
+### 7. Event Sorting (FR-004)
+
+**Decision**: Sort in-memory after reading from localStorage.
+
+**Rationale**: The list is small (<100 events typically). Sorting on every render is negligible. Sort by `dateTime` ascending (nearest upcoming first), then past events after.
+
+**Implementation approach**:
+- Split events into `upcoming` (dateTime >= now) and `past` (dateTime < now)
+- Sort upcoming ascending (soonest first), past descending (most recent past first)
+- Concatenate: `[...upcoming, ...past]`
+
+### 8. Role Distinction (FR-008 / US-5)
+
+**Decision**: Small badge/label on the event card indicating "Organizer" or "Attendee."
+
+**Rationale**: The data is already available — `organizerToken` present means organizer, `rsvpToken` present (without `organizerToken`) means attendee. A subtle text badge is sufficient; no need for icons or colors.
+
+**Implementation approach**:
+- If `organizerToken` is set → show "Organizer" badge (accent-colored)
+- If `rsvpToken` is set (no `organizerToken`) → show "Attendee" badge (muted)
+- If neither → show no badge (edge case: event stored but no role — could happen with manual localStorage manipulation)
diff --git a/specs/009-list-events/spec.md b/specs/009-list-events/spec.md
new file mode 100644
index 0000000..01d15bc
--- /dev/null
+++ b/specs/009-list-events/spec.md
@@ -0,0 +1,145 @@
+# Feature Specification: Event List on Home Page
+
+**Feature Branch**: `009-list-events`
+**Created**: 2026-03-08
+**Status**: Draft
+**Input**: User description: "man kann auf der hauptseite eine liste an events sehen, sofern sie im localstorage gespeichert sind"
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - View My Events (Priority: P1)
+
+As a returning user, I want to see a list of events I have previously created or interacted with (RSVP'd to) on the home page, so I can quickly navigate back to them without needing to remember or bookmark individual event links.
+
+The home page displays all events stored in the browser's local storage. Each event entry shows the event title and date/time. Tapping an event navigates to its detail page.
+
+**Why this priority**: This is the core value of the feature — without the list, the home page remains a dead end for returning users.
+
+**Independent Test**: Can be fully tested by creating an event (or simulating localStorage entries), returning to the home page, and verifying all stored events appear in a list with correct titles and dates.
+
+**Acceptance Scenarios**:
+
+1. **Given** the user has 3 events stored in localStorage, **When** they visit the home page, **Then** all 3 events are displayed in a list showing title and date/time for each.
+2. **Given** the user has events stored in localStorage, **When** they tap on an event in the list, **Then** they are navigated to the event detail page (`/events/:eventToken`).
+3. **Given** the user has events stored in localStorage, **When** they visit the home page, **Then** events are sorted by date/time (nearest upcoming event first, past events last).
+
+---
+
+### User Story 2 - Empty State (Priority: P2)
+
+As a new user with no stored events, I see an inviting empty state on the home page that encourages me to create my first event or explains how to get started.
+
+**Why this priority**: First-time users need clear guidance. The empty state is the first impression for new users.
+
+**Independent Test**: Can be tested by clearing localStorage and visiting the home page — the empty state message and "Create Event" call-to-action should be visible.
+
+**Acceptance Scenarios**:
+
+1. **Given** no events are stored in localStorage, **When** the user visits the home page, **Then** an empty state message is displayed (e.g., "No events yet") with a prominent "Create Event" button.
+2. **Given** the user has at least one event stored, **When** they visit the home page, **Then** the empty state message is not shown — the event list is displayed instead.
+
+---
+
+### User Story 3 - Remove Event from List (Priority: P3)
+
+As a user, I want to remove an event from my personal list so I can keep my home page tidy and only show events I still care about.
+
+**Why this priority**: Housekeeping capability. Without removal, the list grows indefinitely and becomes cluttered over time.
+
+**Independent Test**: Can be tested by having multiple events in localStorage, removing one from the list, and verifying it disappears from the home page while the others remain.
+
+**Acceptance Scenarios**:
+
+1. **Given** the user has events in their list, **When** they tap the delete icon on an event card, **Then** a confirmation prompt appears asking if they are sure.
+1b. **Given** the user has events in their list, **When** they swipe an event card to the left, **Then** a confirmation prompt appears asking if they are sure.
+2. **Given** the confirmation prompt is shown, **When** the user confirms removal, **Then** the event is removed from localStorage and disappears from the list immediately.
+3. **Given** the confirmation prompt is shown, **When** the user cancels, **Then** the event remains in the list unchanged.
+
+---
+
+### User Story 4 - Past Events Appear Faded (Priority: P2)
+
+As a user, I want events whose date/time has passed to appear visually faded or muted in the list, so I can immediately focus on upcoming events without past events cluttering my attention.
+
+The fading should feel modern and polished — not a blunt grey-out, but a subtle reduction in contrast and saturation that makes past events recede visually while remaining readable and tappable.
+
+**Why this priority**: Without this, past and upcoming events look identical, making the list harder to scan. This is essential for usability once a user has accumulated several events.
+
+**Independent Test**: Can be tested by having both future and past events in localStorage and verifying that past events display with reduced visual prominence while remaining interactive.
+
+**Acceptance Scenarios**:
+
+1. **Given** the user has a past event (dateTime before now) in localStorage, **When** they view the home page, **Then** the event appears with reduced visual prominence (muted colors, lower contrast) compared to upcoming events.
+2. **Given** the user has a past event in the list, **When** they tap on it, **Then** it still navigates to the event detail page — it remains fully interactive.
+3. **Given** the user has both past and upcoming events, **When** they view the home page, **Then** upcoming events appear first (full visual prominence), followed by past events (faded), creating a clear visual hierarchy.
+
+---
+
+### User Story 5 - Visual Distinction for Event Roles (Priority: P3)
+
+As a user, I want to see at a glance whether I am the organizer of an event or just an attendee, so I can quickly identify my responsibilities.
+
+**Why this priority**: Nice-to-have clarity. The data is already available in localStorage (presence of `organizerToken`), so surfacing it improves usability at low effort.
+
+**Independent Test**: Can be tested by having both created events (with organizerToken) and RSVP'd events (with rsvpToken) in localStorage, and verifying they display different visual indicators.
+
+**Acceptance Scenarios**:
+
+1. **Given** the user has a created event (organizerToken present) in localStorage, **When** they view the home page, **Then** the event shows a visual indicator marking them as the organizer (e.g., a badge or label).
+2. **Given** the user has an event with an RSVP (rsvpToken present, no organizerToken) in localStorage, **When** they view the home page, **Then** the event shows a visual indicator marking them as an attendee.
+
+---
+
+### Edge Cases
+
+- What happens when localStorage data is corrupted or contains invalid entries? Events with missing required fields (eventToken, title, dateTime) are silently excluded from the list.
+- What happens when localStorage is unavailable (e.g., private browsing with storage disabled)? The empty state is shown with the "Create Event" button — the app remains functional.
+- What happens when an event's date/time has passed? The event remains in the list but appears visually faded.
+- What happens when the user has a very large number of stored events (e.g., 50+)? The list scrolls naturally. No pagination is needed at this scale since localStorage entries are lightweight.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: System MUST display a list of all events stored in the browser's local storage on the home page.
+- **FR-002**: Each event entry MUST show the event title and the event date/time displayed as a relative time label (e.g., "in 3 days", "yesterday") using `Intl.RelativeTimeFormat`.
+- **FR-003**: Each event entry MUST be tappable/clickable and navigate to the event detail page (`/events/:eventToken`).
+- **FR-004**: Events MUST be sorted by date/time with nearest upcoming events first and past events last.
+- **FR-005**: System MUST display an empty state with a "Create Event" call-to-action when no events are stored.
+- **FR-006a**: Users MUST be able to remove individual events from their local list via a visible delete icon on each event card (primary mechanism, implemented first).
+- **FR-006b**: Users MUST be able to remove individual events via swipe-to-delete gesture (secondary mechanism, implemented separately after FR-006a).
+- **FR-007**: System MUST show a confirmation prompt before removing an event from the list.
+- **FR-008**: System MUST visually distinguish events where the user is the organizer from events where the user is an attendee.
+- **FR-009**: System MUST display past events (dateTime before current time) with reduced visual prominence — muted colors and lower contrast — while keeping them readable and interactive.
+- **FR-010**: System MUST gracefully handle corrupted or incomplete localStorage entries by excluding invalid events from the list.
+- **FR-011**: The "Create Event" button MUST remain accessible on the home page even when events are listed, implemented as a Floating Action Button (FAB) fixed at the bottom-right corner.
+
+### Key Entities
+
+- **Stored Event**: A locally persisted reference to an event the user has interacted with. Contains: event token (unique identifier for navigation), title, date/time, expiry date, and optionally an organizer token (if created by this user) or RSVP token and name (if the user RSVP'd).
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: Users can see all their stored events on the home page within 1 second of page load.
+- **SC-002**: Users can navigate from the home page to any event detail page in a single tap/click.
+- **SC-003**: Users can remove an unwanted event from their list in under 3 seconds (including confirmation).
+- **SC-004**: New users (no stored events) see a clear call-to-action to create their first event.
+- **SC-005**: Users can distinguish their role (organizer vs. attendee) for each event at a glance without opening the event.
+
+## Clarifications
+
+### Session 2026-03-08
+
+- Q: How does the user trigger event removal? → A: Two mechanisms — visible delete icon on each event card (primary, implemented first) and swipe-to-delete gesture (secondary, implemented separately after).
+- Q: Placement of "Create Event" button when events exist? → A: Floating Action Button (FAB) fixed at bottom-right corner.
+- Q: Date/time display format in event list? → A: Relative time labels ("in 3 days", "yesterday") via Intl.RelativeTimeFormat.
+
+## Assumptions
+
+- The existing `useEventStorage` composable and `StoredEvent` interface provide all necessary data for the event list (no backend API calls needed for listing).
+- The event list is purely client-side — there is no server-side "my events" endpoint. Privacy is preserved because events are only known to the user's browser.
+- The event list uses `Intl.RelativeTimeFormat` for relative time labels (FR-002), while the event detail view uses `Intl.DateTimeFormat` for absolute date/time display. Both use the browser's locale (`navigator.language`).
+- The "Create Event" flow (spec 006) already saves events to localStorage, so no changes to event creation are needed.
+- The RSVP flow (spec 008) already saves RSVP data to localStorage, so no changes to RSVP are needed.
diff --git a/specs/009-list-events/tasks.md b/specs/009-list-events/tasks.md
new file mode 100644
index 0000000..66f951a
--- /dev/null
+++ b/specs/009-list-events/tasks.md
@@ -0,0 +1,215 @@
+# Tasks: Event List on Home Page
+
+**Input**: Design documents from `/specs/009-list-events/`
+**Prerequisites**: plan.md, spec.md, research.md, data-model.md
+
+**Tests**: Unit tests (Vitest) and E2E tests (Playwright) are included per constitution (Principle II: Test-Driven Methodology).
+
+**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
+- Include exact file paths in descriptions
+
+---
+
+## Phase 1: Setup (Shared Infrastructure)
+
+**Purpose**: Composable extensions and utility functions shared across all user stories
+
+- [x] T000 Rename router param `:token` to `:eventToken` in `frontend/src/router/index.ts` and update all references in `EventDetailView.vue`, `EventStubView.vue`, and their test files (consistency with `StoredEvent.eventToken` field name)
+- [x] T001 Add `isValidStoredEvent` type guard and validation filter to `frontend/src/composables/useEventStorage.ts` (FR-010)
+- [x] T002 Add `removeEvent(eventToken: string)` function to `frontend/src/composables/useEventStorage.ts` (needed by US3)
+- [x] T003 [P] Create `useRelativeTime` composable in `frontend/src/composables/useRelativeTime.ts` (Intl.RelativeTimeFormat wrapper, FR-002)
+- [x] T004 [P] Add unit tests for `isValidStoredEvent` and `removeEvent` in `frontend/src/composables/__tests__/useEventStorage.spec.ts`
+- [x] T005 [P] Create unit tests for `useRelativeTime` in `frontend/src/composables/__tests__/useRelativeTime.spec.ts`
+
+**Checkpoint**: Composable layer complete — all shared logic tested and available for components.
+
+---
+
+## Phase 2: User Story 1 — View My Events (Priority: P1) 🎯 MVP
+
+**Goal**: Home page shows all stored events in a sorted list with title and relative time. Tapping navigates to event detail.
+
+**Independent Test**: Simulate localStorage entries, visit home page, verify all events appear sorted with correct titles and relative times. Tap an event and verify navigation to `/events/:eventToken`.
+
+### Unit Tests for User Story 1
+
+- [x] T006 [P] [US1] Create unit tests for EventCard component in `frontend/src/components/__tests__/EventCard.spec.ts` — include test cases for `isPast` prop (faded styling) and role badge rendering (organizer vs. attendee)
+- [x] T007 [P] [US1] Create unit tests for EventList component in `frontend/src/components/__tests__/EventList.spec.ts`
+
+### Implementation for User Story 1
+
+- [x] T008 [P] [US1] Create `EventCard.vue` component in `frontend/src/components/EventCard.vue` — displays title, relative time, role badge; emits click for navigation
+- [x] T009 [US1] Create `EventList.vue` component in `frontend/src/components/EventList.vue` — reads events from composable, validates, sorts (upcoming asc, past desc), renders EventCard list
+- [x] T010 [US1] Refactor `HomeView.vue` in `frontend/src/views/HomeView.vue` — integrate EventList, conditionally show list when events exist
+- [x] T011 [US1] Add event card and list styles to `frontend/src/assets/main.css`
+
+### E2E Tests for User Story 1
+
+- [x] T012 [US1] Create E2E test file `frontend/e2e/home-events.spec.ts` — tests: events displayed with title and relative time, sorted correctly, click navigates to detail page
+
+**Checkpoint**: MVP complete — returning users see their events and can navigate to details.
+
+---
+
+## Phase 3: User Story 2 — Empty State (Priority: P2)
+
+**Goal**: New users with no stored events see an inviting empty state with a "Create Event" call-to-action.
+
+**Independent Test**: Clear localStorage, visit home page, verify empty state message and "Create Event" button are visible.
+
+### Unit Tests for User Story 2
+
+- [x] T013 [P] [US2] Create unit tests for EmptyState component in `frontend/src/components/__tests__/EmptyState.spec.ts`
+
+### Implementation for User Story 2
+
+- [x] T014 [US2] Create `EmptyState.vue` component in `frontend/src/components/EmptyState.vue` — shows message and "Create Event" RouterLink
+- [x] T015 [US2] Update `HomeView.vue` in `frontend/src/views/HomeView.vue` — show EmptyState when no valid events, show EventList otherwise
+- [x] T016 [US2] Add empty state styles to `frontend/src/assets/main.css`
+
+### E2E Tests for User Story 2
+
+- [x] T017 [US2] Add E2E tests to `frontend/e2e/home-events.spec.ts` — tests: empty state shown when no events, hidden when events exist
+
+**Checkpoint**: Home page handles both new and returning users.
+
+---
+
+## Phase 4: User Story 4 — Past Events Appear Faded (Priority: P2)
+
+**Goal**: Events whose date/time has passed appear with reduced visual prominence (muted colors, lower contrast) while remaining interactive.
+
+**Independent Test**: Have both future and past events in localStorage, verify past events display faded while remaining clickable.
+
+### Implementation for User Story 4
+
+- [x] T018 [US4] Add `.event-card--past` modifier class with `opacity: 0.6; filter: saturate(0.5)` to `frontend/src/components/EventCard.vue` or `frontend/src/assets/main.css`
+- [x] T019 [US4] Pass `isPast` computed property to EventCard in `EventList.vue` and apply modifier class in `frontend/src/components/EventCard.vue`
+
+### E2E Tests for User Story 4
+
+- [x] T020 [US4] Add E2E tests to `frontend/e2e/home-events.spec.ts` — tests: past events have faded class, upcoming events do not, past events remain clickable
+
+**Checkpoint**: Visual hierarchy distinguishes upcoming from past events.
+
+---
+
+## Phase 5: User Story 3 — Remove Event from List (Priority: P3)
+
+**Goal**: Users can remove events from their local list via delete icon (and later swipe) with confirmation.
+
+**Independent Test**: Have multiple events, remove one via delete icon, verify it disappears while others remain.
+
+### Unit Tests for User Story 3
+
+- [x] T021 [P] [US3] Create unit tests for ConfirmDialog component in `frontend/src/components/__tests__/ConfirmDialog.spec.ts`
+
+### Implementation for User Story 3
+
+- [x] T022 [US3] Create `ConfirmDialog.vue` component in `frontend/src/components/ConfirmDialog.vue` — teleport-to-body modal with confirm/cancel, focus trapping, Escape key
+- [x] T023 [US3] Add delete icon button to `EventCard.vue` in `frontend/src/components/EventCard.vue` — emits `delete` event with eventToken (FR-006a)
+- [x] T024 [US3] Wire delete flow in `EventList.vue` in `frontend/src/components/EventList.vue` — listen for delete event, show ConfirmDialog, call `removeEvent()` on confirm
+- [x] T025 [US3] Add delete icon and confirm dialog styles to `frontend/src/assets/main.css`
+
+### E2E Tests for User Story 3
+
+- [x] T026 [US3] Add E2E tests to `frontend/e2e/home-events.spec.ts` — tests: delete icon visible, confirmation dialog appears, confirm removes event, cancel keeps event
+
+**Checkpoint**: Users can manage their event list.
+
+---
+
+## Phase 6: User Story 5 — Visual Distinction for Event Roles (Priority: P3)
+
+**Goal**: Events show a badge indicating whether the user is the organizer or an attendee.
+
+**Independent Test**: Have events with organizerToken and rsvpToken in localStorage, verify different badges displayed.
+
+### Implementation for User Story 5
+
+- [x] T027 [US5] Add role badge (Organizer/Attendee) to `EventCard.vue` in `frontend/src/components/EventCard.vue` — derive from organizerToken/rsvpToken presence
+- [x] T028 [US5] Add role badge styles to `frontend/src/assets/main.css`
+
+### E2E Tests for User Story 5
+
+- [x] T029 [US5] Add E2E tests to `frontend/e2e/home-events.spec.ts` — tests: organizer badge shown for events with organizerToken, attendee badge for events with rsvpToken only
+
+**Checkpoint**: Role distinction visible at a glance.
+
+---
+
+## Phase 7: Polish & Cross-Cutting Concerns
+
+**Purpose**: FAB, swipe gesture, accessibility, and final polish
+
+- [x] T030 Create `CreateEventFab.vue` in `frontend/src/components/CreateEventFab.vue` — fixed FAB at bottom-right, navigates to `/create` (FR-011)
+- [x] T031 Add FAB to `HomeView.vue` in `frontend/src/views/HomeView.vue` — visible when events exist (empty state has its own CTA)
+- [x] T032 Add FAB styles to `frontend/src/assets/main.css`
+- [x] T033 Implement swipe-to-delete gesture on EventCard in `frontend/src/components/EventCard.vue` — native Touch API (FR-006b)
+- [x] T034 Accessibility review: verify ARIA labels, keyboard navigation (Tab/Enter/Escape), focus trapping in ConfirmDialog, WCAG AA contrast on faded cards
+- [x] T035 Add E2E tests for FAB to `frontend/e2e/home-events.spec.ts` — tests: FAB visible when events exist, navigates to create page
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Phase 1 (Setup)**: No dependencies — start immediately
+- **Phase 2 (US1)**: Depends on T001, T003 (validation + relative time composable)
+- **Phase 3 (US2)**: Depends on T001 (validation); can run in parallel with US1
+- **Phase 4 (US4)**: Depends on Phase 2 completion (EventCard must exist)
+- **Phase 5 (US3)**: Depends on Phase 2 completion (EventList must exist) + T002 (removeEvent)
+- **Phase 6 (US5)**: Depends on Phase 2 completion (EventCard must exist)
+- **Phase 7 (Polish)**: Depends on Phases 2–6 completion
+
+### User Story Dependencies
+
+- **US1 (P1)**: Depends only on Phase 1 — no other story dependencies
+- **US2 (P2)**: Depends only on Phase 1 — independent of US1 but shares HomeView
+- **US4 (P2)**: Depends on US1 (extends EventCard with past styling)
+- **US3 (P3)**: Depends on US1 (extends EventList with delete flow)
+- **US5 (P3)**: Depends on US1 (extends EventCard with role badge)
+
+### Parallel Opportunities
+
+- T003 + T004 + T005 can all run in parallel (different files)
+- T006 + T007 can run in parallel (different test files)
+- T008 can run in parallel with T006/T007 (component vs test files)
+- US4, US5 can start in parallel once US1 is done (both extend EventCard independently)
+
+---
+
+## Implementation Strategy
+
+### MVP First (User Story 1 Only)
+
+1. Complete Phase 1: Setup composables
+2. Complete Phase 2: US1 — EventCard, EventList, HomeView refactor
+3. **STOP and VALIDATE**: Test the event list end-to-end
+4. Deploy/demo if ready
+
+### Incremental Delivery
+
+1. Phase 1 → Composable layer ready
+2. Phase 2 (US1) → Event list works → MVP!
+3. Phase 3 (US2) → Empty state for new users
+4. Phase 4 (US4) → Past events faded
+5. Phase 5 (US3) → Remove events from list
+6. Phase 6 (US5) → Role badges
+7. Phase 7 → FAB, swipe, accessibility polish
+
+---
+
+## Notes
+
+- [P] tasks = different files, no dependencies
+- [Story] label maps task to specific user story for traceability
+- This is a **frontend-only** feature — no backend changes needed
+- All data comes from existing `useEventStorage` composable (localStorage)
+- E2E tests consolidated in single file `home-events.spec.ts` with separate `describe` blocks per story