Implement watch-event feature (017) with bookmark in RsvpBar
All checks were successful
CI / backend-test (push) Successful in 1m0s
CI / frontend-test (push) Successful in 27s
CI / frontend-e2e (push) Successful in 1m30s
CI / build-and-publish (push) Has been skipped

Add client-side watch/bookmark functionality: users can save events to
localStorage without RSVPing via a bookmark button next to the "I'm attending"
CTA. Watched events appear in the event list with a "Watching" label.
Bookmark is only visible for visitors (not attendees or organizers).

Includes spec, plan, research, tasks, unit tests, and E2E tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:20:57 +01:00
parent e01d5ee642
commit c450849e4d
22 changed files with 1266 additions and 31 deletions

View File

@@ -0,0 +1,153 @@
# Feature Specification: Watch Event
**Feature Branch**: `017-watch-event`
**Created**: 2026-03-12
**Status**: Draft
**Input**: Watch/bookmark events locally without RSVPing — bookmark icon on event detail page, watching label on event list
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Watch an event from the detail page (Priority: P1)
A user receives a link to an event and opens the detail page. They are not ready to RSVP yet but want to save the event for later. They tap the bookmark icon to the left of the event title. The icon fills in, and the event is saved to their local device. Next time they open the app, the event appears in their event list with a "Watching" label.
**Why this priority**: This is the core feature — without it, the only way to save an event is to RSVP. Many users want to "bookmark" events they're considering without committing.
**Independent Test**: Can be fully tested by opening an event detail page, tapping the bookmark icon, verifying it fills in, and checking the event list shows the event with a "Watching" label.
**Acceptance Scenarios**:
1. **Given** a user visits an event detail page for the first time (no RSVP, no watch), **When** they look at the bookmark icon next to the title, **Then** the icon is displayed as an unfilled outline.
2. **Given** the bookmark icon is unfilled, **When** the user taps it, **Then** the icon becomes filled, and the event is saved to localStorage.
3. **Given** the user has watched an event, **When** they open the event list, **Then** the event appears with a "Watching" label.
---
### User Story 2 - Un-watch an event from the detail page (Priority: P1)
A user who is watching an event (but not attending) decides they are no longer interested. They tap the filled bookmark icon on the event detail page. The icon reverts to an outline, and the event is removed from localStorage. It disappears from the event list.
**Why this priority**: Users must be able to undo the watch action. Without this, there is no way to remove a watched event from the detail page.
**Independent Test**: Can be fully tested by watching an event, then tapping the bookmark icon again to un-watch, verifying the icon becomes unfilled and the event disappears from the event list.
**Acceptance Scenarios**:
1. **Given** a user is watching an event (not attending), **When** they tap the filled bookmark icon, **Then** the icon becomes unfilled and the event is removed from localStorage.
2. **Given** a user has un-watched an event, **When** they open the event list, **Then** the event no longer appears.
---
### User Story 3 - Bookmark icon reflects attending status (Priority: P1)
When a user RSVPs to an event, the event is automatically saved to localStorage. The bookmark icon on the detail page reflects this by appearing filled. Attending supersedes watching — the event list shows "Attendee" (not "Watching") as the label.
**Why this priority**: Consistency — attending inherently means the event is saved locally, so the bookmark must reflect that. Without this, users would see a confusing unfilled bookmark despite having RSVPed.
**Independent Test**: Can be fully tested by RSVPing to an event, verifying the bookmark icon is filled, and checking that the event list shows "Attendee" as the label.
**Acceptance Scenarios**:
1. **Given** a user has RSVPed to an event, **When** they view the event detail page, **Then** the bookmark icon is filled.
2. **Given** a user has RSVPed to an event, **When** they view the event list, **Then** the event shows "Attendee" as its label (not "Watching").
---
### User Story 4 - RSVP cancellation preserves watch status (Priority: P2)
A user who RSVPed to an event cancels their attendance. The event remains in localStorage (existing behavior). The bookmark icon stays filled. The event list label changes from "Attendee" to "Watching".
**Why this priority**: This ensures a smooth transition from attending to watching. Without it, users who cancel would see an event in their list with no label and an ambiguous bookmark state.
**Independent Test**: Can be fully tested by RSVPing, cancelling the RSVP, then verifying the bookmark stays filled and the event list shows "Watching".
**Acceptance Scenarios**:
1. **Given** a user has RSVPed and then cancelled their RSVP, **When** they view the event detail page, **Then** the bookmark icon remains filled.
2. **Given** a user has RSVPed and then cancelled their RSVP, **When** they view the event list, **Then** the event shows "Watching" as its label.
3. **Given** a user cancelled their RSVP and the bookmark is filled, **When** they tap the bookmark icon, **Then** the icon becomes unfilled and the event is removed from localStorage.
---
### User Story 5 - Bookmark icon is non-interactive for attendees and organizers (Priority: P2)
When a user is an attendee or organizer, the bookmark icon is filled but not clickable (no pointer cursor, no hover effect). Tapping it triggers a short shake animation on the relevant fixed action button at the bottom of the screen (the "You're attending" bar for attendees, the "Cancel event" button for organizers) to signal that the user must act there first.
**Why this priority**: Prevents confusion — removing a saved event while attending or organizing must go through the proper flow (cancel RSVP or cancel event), not through the bookmark.
**Independent Test**: Can be fully tested by RSVPing to an event, tapping the bookmark icon, and verifying nothing happens except the bottom bar shaking briefly.
**Acceptance Scenarios**:
1. **Given** a user is an attendee, **When** they tap the bookmark icon, **Then** nothing changes, and the "You're attending" bar shakes briefly.
2. **Given** a user is an organizer, **When** they tap the bookmark icon, **Then** nothing changes, and the "Cancel event" button shakes briefly.
3. **Given** a user is an attendee or organizer, **When** they hover/focus the bookmark icon, **Then** no pointer cursor or interactive hover style is shown.
---
### User Story 6 - Un-watch from event list (Priority: P2)
A watcher removes an event from the event list using the existing swipe-to-delete gesture. Unlike attendees (who see a confirmation dialog warning about RSVP cancellation), watchers see no confirmation dialog — the event is removed immediately.
**Why this priority**: Watching is a low-commitment action, so removing a watched event should be frictionless.
**Independent Test**: Can be fully tested by watching an event, going to the event list, swiping to delete, and verifying the event is removed without a confirmation dialog.
**Acceptance Scenarios**:
1. **Given** a user is watching an event (no RSVP), **When** they swipe to delete it from the event list, **Then** the event is removed immediately without a confirmation dialog.
2. **Given** a user is attending an event, **When** they swipe to delete it from the event list, **Then** a confirmation dialog appears warning about RSVP cancellation (existing behavior, unchanged).
---
### User Story 7 - Watcher upgrades to attendee (Priority: P2)
A user who is watching an event decides to attend. They tap the "I'm attending" CTA button and complete the RSVP flow as usual. The bookmark icon remains filled. The event list label changes from "Watching" to "Attendee".
**Why this priority**: Natural flow from browsing to commitment. The watch-to-attend transition must be seamless.
**Independent Test**: Can be fully tested by watching an event, then RSVPing, and verifying the bookmark stays filled and the label updates.
**Acceptance Scenarios**:
1. **Given** a user is watching an event, **When** they complete the RSVP flow, **Then** the bookmark icon remains filled.
2. **Given** a user was watching and then RSVPed, **When** they view the event list, **Then** the event shows "Attendee" as its label (not "Watching").
---
### Edge Cases
- What happens when a user opens an event that has been cancelled — can they still watch it? **Yes, watching is purely local and independent of event status.**
- What happens when a user watches an event that has expired? **Same behavior — expired events can be watched. They will appear in the "Past" section of the event list.**
- What happens when a user clears their browser localStorage? **All watched (and attended) events are lost. This is expected behavior for client-side-only storage.**
- What happens if the user visits the event page on a different device? **The watch status is device-specific. The bookmark appears unfilled on the new device.**
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The system MUST display a bookmark icon to the left of the event title on the event detail page, vertically centered with the title text.
- **FR-002**: The bookmark icon MUST appear as an unfilled outline when the event is not saved in localStorage.
- **FR-003**: The bookmark icon MUST appear as a filled icon when the event is saved in localStorage (regardless of whether the user is watching, attending, or organizing).
- **FR-004**: Tapping the unfilled bookmark icon MUST save the event to localStorage (eventToken, title, dateTime) and fill the icon.
- **FR-005**: Tapping the filled bookmark icon MUST remove the event from localStorage and revert the icon to unfilled — but only when the user is a watcher (no RSVP, no organizer token).
- **FR-006**: The bookmark icon MUST NOT be interactive (no pointer cursor, no hover effect) when the user is an attendee or organizer.
- **FR-007**: Tapping the bookmark icon as an attendee MUST trigger a brief shake animation on the fixed "You're attending" bar at the bottom.
- **FR-008**: Tapping the bookmark icon as an organizer MUST trigger a brief shake animation on the fixed "Cancel event" button at the bottom.
- **FR-009**: The event list MUST display a "Watching" label on events that are in localStorage but have no rsvpToken and no organizerToken.
- **FR-010**: The "Watching" label MUST have lower precedence than "Attendee" and "Organizer" labels.
- **FR-011**: Deleting a watched event (no RSVP) from the event list MUST NOT show a confirmation dialog — the event is removed immediately.
- **FR-012**: Deleting an attended event from the event list MUST continue to show the existing confirmation dialog with the RSVP cancellation warning.
- **FR-013**: The watch feature MUST be entirely client-side — no server requests are made when watching or un-watching.
- **FR-014**: When an attendee cancels their RSVP, the event MUST remain in localStorage and the bookmark icon MUST remain filled. The event list label MUST change from "Attendee" to "Watching".
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can watch an event in a single tap from the detail page.
- **SC-002**: Watched events appear in the event list with a "Watching" label immediately upon returning to the list.
- **SC-003**: Un-watching an event from the detail page takes a single tap and immediately updates the icon.
- **SC-004**: Deleting a watched event from the event list completes instantly with no confirmation step.
- **SC-005**: The bookmark icon correctly reflects the stored state on every page load (filled if saved, unfilled if not).
- **SC-006**: The transition from watching to attending (and back via RSVP cancellation) updates both the bookmark icon and the event list label without requiring a page reload.