Each section of the user settings page (Profile, Account, Notifications, API Access, Data & Privacy) is now a real, linkable path (/settings/profile, /settings/account, etc.) instead of local toggle state, so sections can be bookmarked, shared, opened in a new tab, and reached via the browser back/forward buttons. - Route now matches an optional segment (settings/:section?), keeping the single route so the shared loader/action and form submissions are unchanged. An unknown section redirects back to /settings. - The shared settings nav components render proper <Link>s when given a buildHref/backHref, and keep their button/onClick behaviour for the league settings page (which has unsaved-changes guards). - The settings page derives the active section and mobile grid/section view from the URL param instead of useState. - Notifications "Account settings" link and the Discord OAuth callback now point at /settings/account. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jt8Hhsio4bHGMaDZy7ftBs
127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
import { Link, useFetcher } from "react-router";
|
|
import { Switch } from "~/components/ui/switch";
|
|
import { Label } from "~/components/ui/label";
|
|
|
|
type ActionResult = { success?: boolean; error?: string };
|
|
|
|
function feedbackFromFetcher(fetcher: ReturnType<typeof useFetcher>) {
|
|
if (fetcher.state !== "idle" || !fetcher.data) return { success: false, error: undefined };
|
|
const data = fetcher.data as ActionResult;
|
|
return { success: !!data.success, error: data.error };
|
|
}
|
|
|
|
type Props = {
|
|
discordPingEnabled: boolean;
|
|
hasDiscordLinked: boolean;
|
|
draftEmailNotificationsEnabled: boolean;
|
|
};
|
|
|
|
export function NotificationsSection({ discordPingEnabled, hasDiscordLinked, draftEmailNotificationsEnabled }: 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 { success: discordSuccess, error: discordError } = feedbackFromFetcher(discordFetcher);
|
|
const { success: emailSuccess } = feedbackFromFetcher(emailFetcher);
|
|
|
|
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>
|
|
{emailSuccess && (
|
|
<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{" "}
|
|
<Link
|
|
to="/settings/account"
|
|
className="text-primary underline-offset-4 hover:underline"
|
|
>
|
|
Account settings
|
|
</Link>{" "}
|
|
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>
|
|
{discordSuccess && (
|
|
<p className="text-sm text-green-600">Notification preference saved.</p>
|
|
)}
|
|
{discordError && (
|
|
<p className="text-sm text-destructive">{discordError}</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|