brackt/app/components/user/settings/ProfileSection.tsx
Chris Parsons 469cc82e15
Add opt-in Discord pings to standings notifications (#429)
* Add opt-in Discord ping to standings notifications

Users who have linked their Discord account can enable a ping preference
in Settings > Notifications. When enabled, their bracket username is replaced
with a Discord @mention (<@userId>) in league standings update messages,
sending them a push notification and showing their Discord display name.

Adds discordPingEnabled column to users, a findDiscordIdsByUserIds model
helper, allowed_mentions support to the Discord webhook payload, and a
NotificationsSection settings component.

https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs

* Remove Display Name field from user profile settings

Username is the only user-facing name field. The display_name column
remains in the database since BetterAuth writes the OAuth provider name
there, and it serves as a programmatic fallback via getUserDisplayName.

https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs

* Address code review feedback on Discord ping feature

- Fix broken Account settings link in NotificationsSection: replace the
  non-functional ?section=account href with an onNavigateToAccount callback
  that drives the parent's useState-based section switcher
- Replace hand-rolled toggle button with the project's Switch component
  (Radix UI) for consistent sizing, accessibility, and dark-mode support
- Guard update-discord-ping action: return an error if the user attempts
  to enable pings without a Discord account linked
- Surface action error in NotificationsSection UI
- Cap allowed_mentions.users at 100 to avoid Discord rejecting the payload
  in large leagues
- Remove the showWinner alias in scoring-calculator; inline winnerScoreChanged
- Add comment to league settings test notification explaining why discordUserId
  is omitted
- Clarify database schema comment: displayName is write-only from BetterAuth

https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 10:06:54 -07:00

97 lines
3 KiB
TypeScript

import { Form } from "react-router";
import { useEffect, useState } from "react";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { TimezoneSelect, TIMEZONE_OPTIONS } from "~/components/league/TimezoneSelect";
import { AvatarEditor } from "~/components/ui/AvatarEditor";
import type { User } from "~/models/user";
const VALID_TIMEZONES = new Set(
TIMEZONE_OPTIONS.flatMap((g) => g.zones.map((z) => z.value))
);
type Props = {
user: User;
isInActiveDraft: boolean;
success?: boolean;
error?: string;
};
export function ProfileSection({ user, isInActiveDraft, success, error }: Props) {
const [timezone, setTimezone] = useState(user.timezone ?? "");
useEffect(() => {
if (!user.timezone) {
try {
const detected = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (detected && VALID_TIMEZONES.has(detected)) {
setTimezone(detected);
}
} catch {
// Intl not available
}
}
}, [user.timezone]);
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold">Profile</h2>
<p className="text-sm text-muted-foreground">
Your username, avatar, and timezone.
</p>
</div>
{success && (
<p className="text-sm text-green-500">Profile updated successfully.</p>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="border-b pb-6">
<AvatarEditor
id={user.id}
flagConfig={user.flagConfig}
uploadedPhotoUrl={user.avatarType === "uploaded" ? user.customAvatarUrl : null}
uploadUrl="/api/upload-avatar"
/>
</div>
<Form method="post" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
name="username"
defaultValue={user.username ?? ""}
/>
</div>
<div className="space-y-1">
<Label>Email</Label>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<input type="hidden" name="timezone" value={timezone} />
<TimezoneSelect
value={timezone}
onChange={setTimezone}
disabled={isInActiveDraft}
/>
{isInActiveDraft ? (
<p className="text-xs text-muted-foreground">
Timezone cannot be changed while you are in an active draft.
</p>
) : (
<p className="text-xs text-muted-foreground">
Used for overnight pick protection. We pre-filled your browser&apos;s detected timezone.
</p>
)}
</div>
<Button type="submit" name="intent" value="update-profile">
Save Changes
</Button>
</Form>
</div>
);
}