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>
236 lines
13 KiB
Markdown
236 lines
13 KiB
Markdown
# 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.
|