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>
3.9 KiB
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.