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

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