Files
fete/specs/017-watch-event/tasks.md
nitrix c450849e4d
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
Implement watch-event feature (017) with bookmark in RsvpBar
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>
2026-03-12 22:20:57 +01:00

13 KiB

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

  • 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
  • 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
  • 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

  • T004 Add saveWatch(eventToken, title, dateTime) and isStored(eventToken) methods to frontend/src/composables/useEventStorage.tssaveWatch creates a StoredEvent with only eventToken/title/dateTime, isStored checks if eventToken exists in storage
  • 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)
  • 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

  • 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
  • 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
  • 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

  • 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

  • T011 Unit test in frontend/src/views/__tests__/EventDetailView.spec.ts — test bookmark icon is filled when event has rsvpToken in storage
  • 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

  • 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

  • T014 Unit test in frontend/src/views/__tests__/EventDetailView.spec.ts — test bookmark stays filled after removeRsvp() is called (event still in storage)
  • 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

  • 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

  • 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
  • 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

  • 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.
  • 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

  • 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
  • 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

  • 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

  • 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

  • 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

  • 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
  • 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
  • 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

# 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.