diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..538e3f3 --- /dev/null +++ b/plan.md @@ -0,0 +1,114 @@ +# Plan: Push Notifications Toggle on Draft Page + +## Overview + +Add a toggle to the draft page sidebar that enables browser push notifications for draft events. When enabled, the user receives desktop notifications when: +1. **It's their turn to pick** (highest priority) +2. **A pick is made by any team** (so they can track the draft from another tab/window) + +This uses the **Browser Notification API** (`Notification.requestPermission()` + `new Notification()`), which is the right fit because: +- Users already have an active Socket.IO connection on the draft page +- The primary use case is "I have the draft tab open but I'm looking at another tab/window" +- No server-side push infrastructure, service workers, or VAPID keys needed +- Simple, client-side-only feature — no database changes required + +## Implementation Steps + +### 1. Create `NotificationSettings` component +**New file:** `app/components/NotificationSettings.tsx` + +A toggle switch component (mirroring `AutodraftSettings.tsx` pattern) that: +- Shows a Switch toggle labeled "Notifications" +- On first enable: calls `Notification.requestPermission()` to request browser permission +- Stores the enabled/disabled preference in `localStorage` (keyed per season, e.g. `draftNotifications-{seasonId}`) +- Shows a small help message if the browser permission is `denied` +- Handles the case where the Notification API is unavailable (e.g. insecure context) by hiding the toggle entirely +- Receives no server props — this is entirely client-side state + +**Props:** +```typescript +interface NotificationSettingsProps { + seasonId: string; + enabled: boolean; + onEnabledChange: (enabled: boolean) => void; +} +``` + +### 2. Create `useDraftNotifications` hook +**New file:** `app/hooks/useDraftNotifications.ts` + +Custom hook that: +- Manages the notification enabled/disabled state (persisted to `localStorage`) +- Checks browser support for Notification API on mount +- Returns `{ notificationsEnabled, setNotificationsEnabled, isSupported }` +- The actual notification firing will happen in the draft page component where socket events are already handled + +### 3. Wire into the draft page +**Modify:** `app/routes/leagues/$leagueId.draft.$seasonId.tsx` + +- Import and use `useDraftNotifications` hook +- In the existing `handlePickMade` socket handler (line ~359), add logic: + - If notifications are enabled and `document.hidden` (tab is not focused): + - Fire a `new Notification()` with the pick info (e.g. "Team X drafted Player Y") + - If the pick means it's now the current user's turn, fire a more urgent notification ("It's your turn to pick!") +- Pass notification state down to the `QueueSection` component + +### 4. Add toggle to the sidebar +**Modify:** `app/components/draft/QueueSection.tsx` + +- Add the `NotificationSettings` component below the `AutodraftSettings` component (inside the same sidebar section) +- Accept new props for notification state + +## UI Placement + +The toggle will sit in the sidebar's queue section, right below the existing Autodraft toggle: + +``` +┌─────────────────────────┐ +│ My Queue │ +│ ┌─────────────────────┐ │ +│ │ 1. Player A [×] │ │ +│ │ 2. Player B [×] │ │ +│ └─────────────────────┘ │ +│ │ +│ ─── Autodraft ──── [·] │ +│ ○ Next Pick │ +│ ○ While On │ +│ │ +│ ─── Notifications ─ [·] │ ← NEW +│ │ +└─────────────────────────┘ +``` + +## Notification Content + +| Event | Condition | Title | Body | +|-------|-----------|-------|------| +| Pick made | Tab not focused, notifications on | "{League} Draft" | "{TeamName} picked {ParticipantName}" | +| Your turn | Tab not focused, notifications on, it's now user's turn | "{League} Draft" | "It's your turn to pick!" | + +Notifications will include `{ tag: "draft-{seasonId}" }` so they replace each other rather than stacking. + +## Edge Cases Handled + +- **Notification API not available** (HTTP, unsupported browser): Toggle hidden entirely via `isSupported` check +- **Permission denied**: Show inline message "Notifications blocked by browser. Update in browser settings." +- **Permission not yet requested**: Request on first toggle enable +- **Tab is focused**: Don't fire notifications (user can already see the draft) +- **SSR**: All Notification API checks guarded with `typeof window !== "undefined"` + +## Files Changed + +| File | Change | +|------|--------| +| `app/hooks/useDraftNotifications.ts` | **New** — hook for notification state + localStorage persistence | +| `app/components/NotificationSettings.tsx` | **New** — toggle UI component | +| `app/components/draft/QueueSection.tsx` | **Modified** — add NotificationSettings below AutodraftSettings | +| `app/routes/leagues/$leagueId.draft.$seasonId.tsx` | **Modified** — use hook, fire notifications in socket handlers, pass props to QueueSection | + +## What This Does NOT Include + +- **Server-side web push** (VAPID keys, web-push library, service worker, push subscriptions table) — not needed for the core use case. Can be added later if users want notifications when the tab is completely closed. +- **Sound notifications** — could be added as a follow-up. +- **Granular notification preferences** (per-event toggles) — a single on/off toggle is sufficient for v1. Can be expanded later. +- **Database storage** — preference stored in localStorage since it's device-specific and doesn't need to sync across devices.