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
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import { useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { Badge } from "~/components/ui/badge";
|
|
import { Button } from "~/components/ui/button";
|
|
import { authClient } from "~/lib/auth-client";
|
|
|
|
type LinkedAccount = {
|
|
id: string;
|
|
providerId: string;
|
|
};
|
|
|
|
type Props = {
|
|
email: string;
|
|
linkedAccounts: LinkedAccount[];
|
|
};
|
|
|
|
const PROVIDER_LABELS: Record<string, string> = {
|
|
credential: "Email & Password",
|
|
google: "Google",
|
|
discord: "Discord",
|
|
};
|
|
|
|
export function AccountSection({ email, linkedAccounts }: Props) {
|
|
const [linkingDiscord, setLinkingDiscord] = useState(false);
|
|
const [linkError, setLinkError] = useState<string | null>(null);
|
|
const hasPassword = linkedAccounts.some((a) => a.providerId === "credential");
|
|
const oauthAccounts = linkedAccounts.filter((a) => a.providerId !== "credential");
|
|
const hasDiscord = linkedAccounts.some((a) => a.providerId === "discord");
|
|
|
|
async function handleConnectDiscord() {
|
|
setLinkingDiscord(true);
|
|
setLinkError(null);
|
|
try {
|
|
await authClient.linkSocial({ provider: "discord", callbackURL: "/settings/account" });
|
|
} catch {
|
|
setLinkError("Failed to connect Discord. Please try again.");
|
|
setLinkingDiscord(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Account</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
Your login email and connected providers.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg border p-4 space-y-3">
|
|
<h3 className="text-sm font-medium">Email Address</h3>
|
|
<p className="text-sm text-muted-foreground">{email}</p>
|
|
</div>
|
|
|
|
<div className="rounded-lg border p-4 space-y-3">
|
|
<h3 className="text-sm font-medium">Sign-in Methods</h3>
|
|
<div className="space-y-2">
|
|
{hasPassword && (
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm">Email & Password</span>
|
|
<Badge variant="secondary">Active</Badge>
|
|
</div>
|
|
)}
|
|
{oauthAccounts.map((account) => (
|
|
<div key={account.id} className="flex items-center justify-between">
|
|
<span className="text-sm">
|
|
{PROVIDER_LABELS[account.providerId] ?? account.providerId}
|
|
</span>
|
|
<Badge variant="secondary">Connected</Badge>
|
|
</div>
|
|
))}
|
|
{linkedAccounts.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">No sign-in methods found.</p>
|
|
)}
|
|
</div>
|
|
{!hasDiscord && (
|
|
<div className="space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleConnectDiscord}
|
|
disabled={linkingDiscord}
|
|
>
|
|
{linkingDiscord ? "Redirecting…" : "Connect Discord"}
|
|
</Button>
|
|
{linkError && (
|
|
<p className="text-sm text-destructive">{linkError}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasPassword && (
|
|
<div className="rounded-lg border p-4 space-y-2">
|
|
<h3 className="text-sm font-medium">Password</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
To change your password, use the forgot password flow.
|
|
</p>
|
|
<Link
|
|
to="/forgot-password"
|
|
className="text-sm text-primary underline-offset-4 hover:underline"
|
|
>
|
|
Reset password
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|