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