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>
This commit is contained in:
56
specs/017-watch-event/research.md
Normal file
56
specs/017-watch-event/research.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Research: Watch Event
|
||||
|
||||
**Feature**: 017-watch-event
|
||||
**Date**: 2026-03-12
|
||||
|
||||
## Research Questions
|
||||
|
||||
### 1. How does the current role detection work?
|
||||
|
||||
**Finding**: `EventList.vue` has a `getRole()` function that checks `organizerToken` first, then `rsvpToken`. Returns `undefined` when neither is present. `EventCard.vue` accepts an `eventRole` prop typed as `'organizer' | 'attendee' | undefined`.
|
||||
|
||||
**Decision**: Extend `getRole()` to return `'watcher'` when the event is in localStorage but has no `organizerToken` and no `rsvpToken`. Extend `EventCard` prop type to include `'watcher'`.
|
||||
|
||||
**Rationale**: This is the minimal change — the existing priority chain (organizer > attendee) already handles precedence. Adding watcher as the fallback case is natural.
|
||||
|
||||
### 2. How to detect "is this event stored?" on the detail page?
|
||||
|
||||
**Finding**: `useEventStorage` has `getStoredEvents()` which returns all events, and `getRsvp(eventToken)` / `getOrganizerToken(eventToken)` for specific lookups. There is no direct `isStored(eventToken)` check.
|
||||
|
||||
**Decision**: Add a `isStored(eventToken)` method to `useEventStorage` that checks if an event exists in localStorage regardless of role. Add a `saveWatch(eventToken, title, dateTime)` method that creates a minimal StoredEvent entry (no rsvpToken, no organizerToken).
|
||||
|
||||
**Rationale**: `saveWatch()` is semantically distinct from `saveRsvp()` and `saveCreatedEvent()`. The `isStored()` helper avoids filtering through the full event list for a simple boolean check.
|
||||
|
||||
### 3. What happens to events after RSVP cancellation?
|
||||
|
||||
**Finding**: `removeRsvp(eventToken)` deletes `rsvpToken` and `rsvpName` but keeps the event in localStorage. After cancellation, the event has no `rsvpToken` and no `organizerToken` — identical to a watched event.
|
||||
|
||||
**Decision**: No change needed. The existing `removeRsvp()` behavior already produces the correct state for a "watcher" after cancellation. The `getRole()` update will automatically label these as "Watching".
|
||||
|
||||
**Rationale**: This is the key insight — the post-RSVP-cancellation state is already semantically equivalent to "watching". We just need to label it.
|
||||
|
||||
### 4. Bookmark icon placement and glow conflict
|
||||
|
||||
**Finding**: The event title is a plain `<h1 class="detail__title">`. The RsvpBar CTA uses `glow-border glow-border--animated` with a `::before` pseudo-element that extends 12px beyond the button via `inset: -4px` + `blur(8px)`. The bookmark icon is positioned at the title area (top of page), far from the RsvpBar (fixed at bottom). No glow conflict.
|
||||
|
||||
**Decision**: Place bookmark icon in a flex container with the title: `display: flex; align-items: center; gap: var(--spacing-sm)`. Icon to the left, title takes remaining space.
|
||||
|
||||
**Rationale**: Vertically centered with flex is the simplest approach. No glow interference since the icon is nowhere near the RsvpBar.
|
||||
|
||||
### 5. Delete confirmation behavior per role
|
||||
|
||||
**Finding**: `EventList.vue` shows a `ConfirmDialog` for all deletions. The message text varies based on RSVP status. For events without RSVP, the message is generic ("This event will be removed from your list.").
|
||||
|
||||
**Decision**: Skip the confirmation dialog entirely for watchers (no `rsvpToken`, no `organizerToken`). Call `removeEvent()` directly on swipe/delete.
|
||||
|
||||
**Rationale**: Watching is low-commitment. The spec explicitly requires no confirmation for watcher deletion.
|
||||
|
||||
### 6. Shake animation implementation
|
||||
|
||||
**Finding**: No existing shake animation in the codebase. The RsvpBar status and cancel-event button are both `position: fixed; bottom: 0`.
|
||||
|
||||
**Decision**: Add a CSS `@keyframes shake` animation (short horizontal oscillation, ~300ms). Apply via a reactive class that is toggled on bookmark tap when user is attendee/organizer. Use a ref + setTimeout to remove the class after animation completes.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Web Animations API: More flexible but overkill for a simple shake.
|
||||
- CSS transition: Insufficient for a multi-step oscillation.
|
||||
Reference in New Issue
Block a user