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,235 @@
# Tasks: Watch Event
**Input**: Design documents from `/specs/017-watch-event/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
**Tests**: Included — constitution mandates TDD (Red → Green → Refactor).
**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: Foundational (Composable & Data Layer)
**Purpose**: Extend `useEventStorage` with watch capabilities and update role detection across list components. These changes are required by all user stories.
**⚠️ CRITICAL**: No user story work can begin until this phase is complete.
### Tests
- [x] T001 [P] Unit tests for `saveWatch()` and `isStored()` methods in `frontend/src/composables/__tests__/useEventStorage.spec.ts` — test saving a watch-only event (no rsvpToken, no organizerToken), test `isStored()` returns true for watched/attended/organized events and false for unknown tokens
- [x] T002 [P] Unit tests for watcher role detection in `frontend/src/components/__tests__/EventList.spec.ts` — test `getRole()` returns `'watcher'` when event has no organizerToken and no rsvpToken
- [x] T003 [P] Unit tests for watcher badge display in `frontend/src/components/__tests__/EventCard.spec.ts` — test that `eventRole="watcher"` renders badge with text "Watching"
### Implementation
- [x] T004 Add `saveWatch(eventToken, title, dateTime)` and `isStored(eventToken)` methods to `frontend/src/composables/useEventStorage.ts``saveWatch` creates a StoredEvent with only eventToken/title/dateTime, `isStored` checks if eventToken exists in storage
- [x] T005 Update `getRole()` in `frontend/src/components/EventList.vue` to return `'watcher'` as fallback when event has no organizerToken and no rsvpToken (role hierarchy: organizer > attendee > watcher)
- [x] T006 [P] Extend `eventRole` prop type in `frontend/src/components/EventCard.vue` from `'organizer' | 'attendee'` to `'organizer' | 'attendee' | 'watcher'`, add "Watching" label text and `.event-card__badge--watcher` styling (glass style, matching design system)
**Checkpoint**: Composable supports watch storage, role detection returns 'watcher', event cards display "Watching" badge.
---
## Phase 2: User Story 1 & 2 — Watch / Un-watch from Detail Page (Priority: P1) 🎯 MVP
**Goal**: Add bookmark icon left of event title on detail page. Unfilled = not stored, filled = stored. Tapping toggles watch state for non-attendee/non-organizer users.
**Independent Test**: Open an event detail page, tap bookmark to watch (icon fills, event appears in list with "Watching" label), tap again to un-watch (icon unfills, event disappears from list).
### Tests
- [x] T007 Unit tests for bookmark icon in `frontend/src/views/__tests__/EventDetailView.spec.ts` — test icon renders unfilled when event not in storage, test icon renders filled when event is in storage, test tapping unfilled icon calls `saveWatch()`, test tapping filled icon calls `removeEvent()` when user is watcher
- [x] T008 E2E test for US1 (watch) in `frontend/e2e/watch-event.spec.ts` — visit event detail page, verify bookmark is unfilled, tap bookmark, verify it fills, navigate to event list, verify event appears with "Watching" label
- [x] T009 E2E test for US2 (un-watch) in `frontend/e2e/watch-event.spec.ts` — watch an event, tap filled bookmark, verify it unfills, navigate to event list, verify event is gone
### Implementation
- [x] T010 [US1] [US2] Add bookmark icon to `frontend/src/views/EventDetailView.vue` — wrap title in flex container (`display: flex; align-items: center; gap: var(--spacing-sm)`), add bookmark button to the left of `<h1>`, icon is unfilled outline when `!isStored(eventToken)` and filled when `isStored(eventToken)`. Tapping calls `saveWatch()` or `removeEvent()` based on current state. Use semantic `<button>` with `aria-label` ("Watch this event" / "Stop watching this event"). Include keyboard support (Enter/Space).
**Checkpoint**: Users can watch and un-watch events from the detail page. Watched events appear in the event list with "Watching" label.
---
## Phase 3: User Story 3 — Bookmark Reflects Attending Status (Priority: P1)
**Goal**: Bookmark icon appears filled when user has RSVPed (attending = automatically watched). Event list shows "Attendee" label, not "Watching".
**Independent Test**: RSVP to an event, verify bookmark is filled on detail page, verify event list shows "Attendee" label.
### Tests
- [x] T011 Unit test in `frontend/src/views/__tests__/EventDetailView.spec.ts` — test bookmark icon is filled when event has rsvpToken in storage
- [x] T012 E2E test for US3 in `frontend/e2e/watch-event.spec.ts` — RSVP to event, verify bookmark is filled, navigate to list, verify "Attendee" label (not "Watching")
### Implementation
- [x] T013 [US3] Verify bookmark icon state in `frontend/src/views/EventDetailView.vue` correctly uses `isStored(eventToken)` which returns true for RSVPed events (since `saveRsvp()` already stores the event). No code change expected — this should work from T010 implementation. If not, adjust `isStored()` logic.
**Checkpoint**: Attending users see filled bookmark. Label priority (Attendee > Watching) works correctly.
---
## Phase 4: User Story 4 — RSVP Cancellation Preserves Watch Status (Priority: P2)
**Goal**: After cancelling RSVP, event stays in localStorage, bookmark stays filled, list label changes from "Attendee" to "Watching".
**Independent Test**: RSVP, cancel RSVP, verify bookmark stays filled and list shows "Watching". Then un-watch via bookmark.
### Tests
- [x] T014 Unit test in `frontend/src/views/__tests__/EventDetailView.spec.ts` — test bookmark stays filled after `removeRsvp()` is called (event still in storage)
- [x] T015 E2E test for US4 in `frontend/e2e/watch-event.spec.ts` — RSVP, cancel attendance, verify bookmark filled, verify list label is "Watching", tap bookmark to un-watch, verify unfilled
### Implementation
- [x] T016 [US4] Verify existing `removeRsvp()` behavior in `frontend/src/composables/useEventStorage.ts` preserves event in storage. No code change expected — `removeRsvp()` already only deletes rsvpToken/rsvpName. The `getRole()` update from T005 will automatically label these as "watcher". If behavior differs, adjust.
**Checkpoint**: RSVP cancel → watch transition works seamlessly.
---
## Phase 5: User Story 5 — Non-Interactive Bookmark for Attendees & Organizers (Priority: P2)
**Goal**: Bookmark icon is visually filled but non-clickable for attendees and organizers. Tapping triggers a shake animation on the relevant fixed bottom button.
**Independent Test**: RSVP to event, tap bookmark, verify nothing changes and "You're attending" bar shakes. Same test for organizer with "Cancel event" button.
### Tests
- [x] T017 Unit test in `frontend/src/views/__tests__/EventDetailView.spec.ts` — test bookmark has no pointer cursor when user is attendee, test tapping bookmark as attendee does not call `removeEvent()`, test shake class is applied to RsvpBar ref
- [x] T018 E2E test for US5 in `frontend/e2e/watch-event.spec.ts` — RSVP to event, tap bookmark, verify bookmark unchanged, verify attending bar has shake animation class. Test organizer: open as organizer, tap bookmark, verify cancel-event button shakes.
### Implementation
- [x] T019 [US5] Add shake animation CSS keyframes in `frontend/src/views/EventDetailView.vue``@keyframes shake` with short horizontal oscillation (~300ms). Add `.detail__shake` class that applies the animation.
- [x] T020 [US5] Update bookmark icon behavior in `frontend/src/views/EventDetailView.vue` — when user is attendee or organizer: remove pointer cursor, remove hover effects, on tap apply shake class to the RsvpBar (attendee) or cancel-event button (organizer) via template ref. Use `setTimeout` to remove shake class after animation completes.
**Checkpoint**: Attendees and organizers cannot un-watch via bookmark. Clear visual feedback via shake.
---
## Phase 6: User Story 6 — Un-watch from Event List (Priority: P2)
**Goal**: Swiping to delete a watched event removes it immediately without a confirmation dialog.
**Independent Test**: Watch an event, go to event list, swipe to delete, verify event removed instantly (no dialog).
### Tests
- [x] T021 Unit test in `frontend/src/components/__tests__/EventList.spec.ts` — test that deleting a watcher event (no rsvpToken) calls `removeEvent()` directly without showing ConfirmDialog
- [x] T022 E2E test for US6 in `frontend/e2e/watch-event.spec.ts` — watch event, navigate to list, swipe to delete, verify no confirmation dialog appears, verify event removed
### Implementation
- [x] T023 [US6] Update delete flow in `frontend/src/components/EventList.vue` — when event has no rsvpToken and no organizerToken (watcher role), skip `showConfirmDialog` and call `removeEvent()` directly. Keep existing confirmation for attendees.
**Checkpoint**: Watcher deletion is frictionless. Attendee deletion unchanged.
---
## Phase 7: User Story 7 — Watcher Upgrades to Attendee (Priority: P2)
**Goal**: A watcher who RSVPs sees bookmark stay filled and list label change from "Watching" to "Attendee".
**Independent Test**: Watch event, RSVP, verify bookmark stays filled, verify list shows "Attendee".
### Tests
- [x] T024 E2E test for US7 in `frontend/e2e/watch-event.spec.ts` — watch event (verify "Watching" in list), RSVP (verify bookmark stays filled), navigate to list (verify "Attendee" label)
### Implementation
- [x] T025 [US7] Verify watch-to-attend transition in `frontend/src/views/EventDetailView.vue` — existing `saveRsvp()` call updates the StoredEvent with rsvpToken/rsvpName. The `getRole()` update from T005 gives "attendee" precedence over "watcher". No code change expected — verify via E2E test.
**Checkpoint**: Watch → attend transition is seamless.
---
## Phase 8: Polish & Cross-Cutting Concerns
**Purpose**: Accessibility, visual refinement, and final validation
- [x] T026 Accessibility audit of bookmark icon in `frontend/src/views/EventDetailView.vue` — verify ARIA labels update reactively ("Watch this event" ↔ "Stop watching this event"), verify keyboard navigation (Tab focus, Enter/Space activation), verify WCAG AA contrast for icon in both states
- [x] T027 Visual consistency check — verify "Watching" badge styling is consistent with existing "Organizer" and "Attendee" badges in `frontend/src/components/EventCard.vue`, follows design system tokens
- [x] T028 Run full E2E suite `frontend/e2e/watch-event.spec.ts` to verify all 7 user stories pass together
---
## Dependencies & Execution Order
### Phase Dependencies
- **Foundational (Phase 1)**: No dependencies — can start immediately
- **US1/US2 (Phase 2)**: Depends on Phase 1 — BLOCKS all other user stories
- **US3 (Phase 3)**: Depends on Phase 2 (bookmark icon must exist)
- **US4 (Phase 4)**: Depends on Phase 2 (bookmark icon must exist)
- **US5 (Phase 5)**: Depends on Phase 2 (bookmark icon must exist)
- **US6 (Phase 6)**: Depends on Phase 1 (getRole must return 'watcher')
- **US7 (Phase 7)**: Depends on Phase 2 (bookmark icon must exist)
- **Polish (Phase 8)**: Depends on all phases complete
### User Story Dependencies
- **US1/US2 (P1)**: Core MVP — can start after Foundational
- **US3 (P1)**: Can start after US1/US2
- **US4 (P2)**: Can start after US1/US2 — independent of US3
- **US5 (P2)**: Can start after US1/US2 — independent of US3/US4
- **US6 (P2)**: Can start after Foundational — independent of all other stories
- **US7 (P2)**: Can start after US1/US2 — independent of US3-US6
### Parallel Opportunities
- **Phase 1**: T001, T002, T003 can run in parallel (different test files)
- **Phase 1**: T005 and T006 can run in parallel (different component files)
- **After Phase 2**: US3, US4, US5, US7 can run in parallel (independent stories)
- **US6**: Can run in parallel with Phase 2 (only depends on Phase 1)
---
## Parallel Example: Phase 1
```text
# All unit tests in parallel:
T001: "Unit tests for saveWatch/isStored in useEventStorage.spec.ts"
T002: "Unit tests for watcher role in EventList.spec.ts"
T003: "Unit tests for watcher badge in EventCard.spec.ts"
# Implementation in parallel (after tests):
T005: "Update getRole() in EventList.vue"
T006: "Extend eventRole in EventCard.vue"
# T004 (useEventStorage) should go first — T005/T006 depend on its types
```
---
## Implementation Strategy
### MVP First (US1 + US2 Only)
1. Complete Phase 1: Foundational (composable + card + list)
2. Complete Phase 2: US1/US2 (bookmark icon toggle)
3. **STOP and VALIDATE**: Watch/un-watch works, "Watching" label appears
4. This alone delivers the core value
### Incremental Delivery
1. Phase 1 → Foundational ready
2. Phase 2 → US1/US2 → Watch/un-watch from detail page (MVP!)
3. Phase 3 → US3 → Bookmark reflects attending (consistency)
4. Phase 4-7 → US4-US7 → Edge cases and transitions
5. Phase 8 → Polish → Accessibility and visual refinement
---
## Notes
- Most "implementation" in US3, US4, US7 is verification — the foundational changes in Phase 1 and the bookmark icon in Phase 2 handle the logic. These stories primarily need E2E tests to confirm correct behavior.
- No backend changes. No new files except `frontend/e2e/watch-event.spec.ts`.
- Total: 28 tasks across 8 phases.