import { redirect, Form } from "react-router"; import { useEffect, useState } from "react"; import { auth } from "~/lib/auth.server"; import { findUserById, updateUser, isUserInActiveDraft } from "~/models/user"; import type { Route } from "./+types/user-profile"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; 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 { parseFlagConfig } from "~/lib/flag-types"; import { deleteCloudinaryImageByUrl } from "~/lib/cloudinary.server"; import { logger } from "~/lib/logger"; const VALID_TIMEZONES = new Set( TIMEZONE_OPTIONS.flatMap((g) => g.zones.map((z) => z.value)) ); export function meta(): Route.MetaDescriptors { return [{ title: "Profile - Brackt" }]; } export async function loader(args: Route.LoaderArgs) { const session = await auth.api.getSession({ headers: args.request.headers }); if (!session) { return redirect("/login?redirectTo=/user-profile"); } const user = await findUserById(session.user.id); if (!user) { return redirect("/"); } const isInActiveDraft = await isUserInActiveDraft(session.user.id); return { user, isInActiveDraft }; } export async function action(args: Route.ActionArgs) { const session = await auth.api.getSession({ headers: args.request.headers }); if (!session) { return redirect("/login?redirectTo=/user-profile"); } const user = await findUserById(session.user.id); if (!user) { return redirect("/"); } const formData = await args.request.formData(); const intent = formData.get("intent"); if (intent === "update-avatar-flag") { const rawConfig = formData.get("flagConfig"); if (typeof rawConfig !== "string") { return { error: "Flag config is required." }; } let parsedConfig: unknown; try { parsedConfig = JSON.parse(rawConfig); } catch { return { error: "Invalid flag config." }; } const flagConfig = parseFlagConfig(parsedConfig); if (!flagConfig) { return { error: "Invalid flag config." }; } await updateUser(session.user.id, { flagConfig, avatarType: "flag", }); return { success: true }; } if (intent === "remove-avatar-photo") { await updateUser(session.user.id, { customAvatarUrl: null, avatarType: "flag", }); if (user.customAvatarUrl) { deleteCloudinaryImageByUrl(user.customAvatarUrl).catch((error) => { logger.error("Failed to delete user avatar from Cloudinary:", error); }); } return { success: true }; } const displayName = formData.get("displayName") as string; const username = formData.get("username") as string | null; const firstName = formData.get("firstName") as string | null; const lastName = formData.get("lastName") as string | null; const timezone = formData.get("timezone") as string | null; const timezoneValue = timezone && VALID_TIMEZONES.has(timezone) ? timezone : undefined; await updateUser(session.user.id, { displayName: displayName || undefined, username: username || undefined, firstName: firstName || undefined, lastName: lastName || undefined, timezone: timezoneValue, }); return { success: true }; } export default function UserProfilePage({ loaderData, actionData }: Route.ComponentProps) { const { user, isInActiveDraft } = loaderData; const [timezone, setTimezone] = useState(user.timezone ?? ""); // Auto-detect browser timezone if not already set useEffect(() => { if (!user.timezone) { try { const detected = Intl.DateTimeFormat().resolvedOptions().timeZone; if (detected && VALID_TIMEZONES.has(detected)) { setTimezone(detected); } } catch { // Intl not available — leave blank } } }, [user.timezone]); return (
Your Profile {actionData?.success && (

Profile updated successfully.

)} {actionData && "error" in actionData && actionData.error && (

{actionData.error}

)}

{user.email}

{/* Hidden input carries the controlled value into the form submission */} {isInActiveDraft ? (

Timezone cannot be changed while you are in an active draft.

) : (

Used for overnight pick protection. We pre-filled your browser's detected timezone.

)}
); }