brackt/app/components/user/settings/ProfileSection.tsx

98 lines
3 KiB
TypeScript
Raw Normal View History

Refactor user profile to comprehensive settings page (#403) * Add account deletion and multi-section settings page - Rename /user-profile → /settings with 301 redirect from old URL - Add multi-section settings nav (Profile, Account, API placeholder, Data & Privacy) reusing existing SettingsDesktopNav/SettingsMobileGridNav components - Implement account deletion via anonymization: wipes all PII from users row, releases team ownerships, removes commissioner records, deletes sessions/accounts - Add data export request form that emails privacy@brackt.com via Resend - Add deletedAt timestamp column to users table (migration 0100) - Add anonymizeUserAccount() to user model - Add removeAllCommissionersByUserId() to commissioner model - Tests for both new model functions - Update UserMenu "Profile" link → "Settings" at /settings https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X * Address code review feedback on settings/account deletion 1. Wrap anonymizeUserAccount in a DB transaction so partial failures (e.g. session delete succeeds but user update fails) can't leave accounts in an inconsistent state 2. Escape user email and notes with escapeHtml() before interpolating into the data request email body 3. Reset AlertDialog confirmed state when dialog is dismissed via backdrop click or Escape key (onOpenChange handler) 4. Extract accounts DB query to app/models/account.ts (findLinkedAccountsByUserId) to comply with the "always query through app/models/" convention 5. Replace dynamic imports() in action handlers with static top-level imports 6. Remove the unused hard-delete deleteUser() function 7. Add lastDataRequestAt timestamp to users (migration 0101) and enforce a 30-day server-side cooldown on data export requests 8. Replace fragile actionData type casts with a proper ActionData discriminated union; narrowing now works without `as` assertions 9. Strengthen tests: verify which schema tables are passed to delete() and that operations run inside the transaction https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X * Fix no-non-null-assertion lint error in PrivacySection Replace non-null assertion with optional chaining on dataRequestCooldownUntil to satisfy oxlint no-non-null-assertion rule. https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 17:26:14 -07:00
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">
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
Your username, avatar, and timezone.
Refactor user profile to comprehensive settings page (#403) * Add account deletion and multi-section settings page - Rename /user-profile → /settings with 301 redirect from old URL - Add multi-section settings nav (Profile, Account, API placeholder, Data & Privacy) reusing existing SettingsDesktopNav/SettingsMobileGridNav components - Implement account deletion via anonymization: wipes all PII from users row, releases team ownerships, removes commissioner records, deletes sessions/accounts - Add data export request form that emails privacy@brackt.com via Resend - Add deletedAt timestamp column to users table (migration 0100) - Add anonymizeUserAccount() to user model - Add removeAllCommissionersByUserId() to commissioner model - Tests for both new model functions - Update UserMenu "Profile" link → "Settings" at /settings https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X * Address code review feedback on settings/account deletion 1. Wrap anonymizeUserAccount in a DB transaction so partial failures (e.g. session delete succeeds but user update fails) can't leave accounts in an inconsistent state 2. Escape user email and notes with escapeHtml() before interpolating into the data request email body 3. Reset AlertDialog confirmed state when dialog is dismissed via backdrop click or Escape key (onOpenChange handler) 4. Extract accounts DB query to app/models/account.ts (findLinkedAccountsByUserId) to comply with the "always query through app/models/" convention 5. Replace dynamic imports() in action handlers with static top-level imports 6. Remove the unused hard-delete deleteUser() function 7. Add lastDataRequestAt timestamp to users (migration 0101) and enforce a 30-day server-side cooldown on data export requests 8. Replace fragile actionData type casts with a proper ActionData discriminated union; narrowing now works without `as` assertions 9. Strengthen tests: verify which schema tables are passed to delete() and that operations run inside the transaction https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X * Fix no-non-null-assertion lint error in PrivacySection Replace non-null assertion with optional chaining on dataRequestCooldownUntil to satisfy oxlint no-non-null-assertion rule. https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 17:26:14 -07:00
</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>
);
}