brackt/app/components/user/settings/NotificationsSection.tsx
Claude 2ef75a1d44
Add on-the-clock email notifications and fix push notification modal
- Add `draft_email_notifications_enabled` column to users table
- New `sendOnTheClockEmail` service sends an email when a user's turn arrives
  in a draft; detects back-to-back picks and sends one combined email instead
  of two; skips notification if the team has autodraft enabled
- Email is triggered after every manual pick (make-pick route) and every
  timer-expiry pick (executeAutoPick) once the full autodraft chain settles,
  so the notified user is always the first human on the clock
- Add email notification toggle to the user settings Notifications section
- Fix push notification dropdown in the draft room: the bare absolute div is
  replaced with a Radix Popover so clicking outside dismisses it without
  toggling notifications off
- Rename draft room label from "Pick Notifications" to "Push Notifications"
  to distinguish from the new email notifications

https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA
2026-05-20 21:33:20 +00:00

120 lines
4.4 KiB
TypeScript

import { useFetcher } from "react-router";
import { Switch } from "~/components/ui/switch";
import { Label } from "~/components/ui/label";
type Props = {
discordPingEnabled: boolean;
hasDiscordLinked: boolean;
draftEmailNotificationsEnabled: boolean;
success?: boolean;
error?: string;
onNavigateToAccount: () => void;
};
export function NotificationsSection({ discordPingEnabled, hasDiscordLinked, draftEmailNotificationsEnabled, success, error, onNavigateToAccount }: Props) {
const discordFetcher = useFetcher();
const emailFetcher = useFetcher();
const optimisticDiscordEnabled =
discordFetcher.state !== "idle"
? discordFetcher.formData?.get("discordPingEnabled") === "true"
: discordPingEnabled;
const optimisticEmailEnabled =
emailFetcher.state !== "idle"
? emailFetcher.formData?.get("draftEmailNotificationsEnabled") === "true"
: draftEmailNotificationsEnabled;
const handleDiscordToggle = (checked: boolean) => {
discordFetcher.submit(
{ intent: "update-discord-ping", discordPingEnabled: String(checked) },
{ method: "post" }
);
};
const handleEmailToggle = (checked: boolean) => {
emailFetcher.submit(
{ intent: "update-email-draft-notifications", draftEmailNotificationsEnabled: String(checked) },
{ method: "post" }
);
};
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold">Notifications</h2>
<p className="text-sm text-muted-foreground">
Manage how you receive league notifications.
</p>
</div>
<div className="rounded-lg border p-4 space-y-4">
<h3 className="text-sm font-medium">Draft Pick Emails</h3>
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="email-draft-switch" className="text-sm font-normal">
Email me when I'm on the clock
</Label>
<p className="text-xs text-muted-foreground">
Receive an email when it's your turn to pick in a draft. If you have back-to-back
picks, you'll get a single email letting you know.
</p>
</div>
<Switch
id="email-draft-switch"
checked={optimisticEmailEnabled}
onCheckedChange={handleEmailToggle}
disabled={emailFetcher.state !== "idle"}
/>
</div>
{emailFetcher.state === "idle" && emailFetcher.data && (emailFetcher.data as { intent?: string; success?: boolean }).intent === "update-email-draft-notifications" && (emailFetcher.data as { success?: boolean }).success && (
<p className="text-sm text-green-600">Email notification preference saved.</p>
)}
</div>
<div className="rounded-lg border p-4 space-y-4">
<h3 className="text-sm font-medium">Discord Pings</h3>
{!hasDiscordLinked ? (
<p className="text-sm text-muted-foreground">
Link your Discord account in{" "}
<button
type="button"
onClick={onNavigateToAccount}
className="text-primary underline-offset-4 hover:underline"
>
Account settings
</button>{" "}
to enable Discord pings for league notifications.
</p>
) : (
<>
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="discord-ping-switch" className="text-sm font-normal">
Ping me in standings updates
</Label>
<p className="text-xs text-muted-foreground">
When enabled, you'll be @mentioned in league Discord notifications and your
Discord display name will appear in place of your brackt username.
</p>
</div>
<Switch
id="discord-ping-switch"
checked={optimisticDiscordEnabled}
onCheckedChange={handleDiscordToggle}
disabled={discordFetcher.state !== "idle"}
/>
</div>
{success && (
<p className="text-sm text-green-600">Notification preference saved.</p>
)}
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
</>
)}
</div>
</div>
);
}