From 6df4f86920b91432538abb905ad2c3efe4f03f4d Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 7 May 2026 10:09:45 -0700 Subject: [PATCH] Improve flag config validation and remove settings completion tracking (#390) * Fix mobile league settings: prevent sports scroll and remove completion checkmarks - Add overflow-hidden to sport label text span so flex truncation is properly contained - Add min-w-0 to SettingsSection header inner flex item to prevent title/description overflow - Remove completion check marks from mobile grid nav buttons (league settings has no completion concept) - Remove "X of Y set" counter from mobile nav header - Remove isComplete field from SettingsGridSection type and all data https://claude.ai/code/session_01T7iTb9YZLuWJFtKV753tdB * Fix race window in user creation and restore FlagSvg safety fallback auth.server.ts: switch generateId to application-generated UUIDs so the ID is known before the insert, then move flagConfig assignment from create.after (a separate UPDATE) to create.before so it lands in the initial INSERT with no race window. FlagSvg.tsx: validate the config with parseFlagConfig before rendering; corrupt or missing data falls back to a neutral gray triband flag rather than silently rendering blank SVG shapes. https://claude.ai/code/session_01T7iTb9YZLuWJFtKV753tdB --------- Co-authored-by: Claude --- .../league/settings/SettingsNavigation.tsx | 17 ++-------- .../league/settings/SettingsSection.tsx | 2 +- .../league/settings/SportsSection.tsx | 2 +- app/components/ui/FlagSvg.tsx | 31 ++++++++++--------- app/lib/auth.server.ts | 14 +++++---- app/routes/leagues/$leagueId.settings.tsx | 18 +++++------ 6 files changed, 38 insertions(+), 46 deletions(-) diff --git a/app/components/league/settings/SettingsNavigation.tsx b/app/components/league/settings/SettingsNavigation.tsx index ec8371c..f5bdcb7 100644 --- a/app/components/league/settings/SettingsNavigation.tsx +++ b/app/components/league/settings/SettingsNavigation.tsx @@ -1,6 +1,6 @@ import type { ComponentType } from "react"; import type { LucideProps } from "lucide-react"; -import { ArrowLeft, Check } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import { cn } from "~/lib/utils"; type SettingsNavSection = { @@ -11,28 +11,22 @@ type SettingsNavSection = { export type SettingsGridSection = SettingsNavSection & { icon: ComponentType; subtitle: string; - isComplete: boolean; isDanger?: boolean; }; export function SettingsMobileGridNav({ sections, - completedCount, onSectionChange, }: { sections: readonly SettingsGridSection[]; - completedCount: number; onSectionChange: (sectionId: string) => void; }) { return (
-
+

Manage

-

- {completedCount} of {sections.length} set -

{sections.map((section) => ( @@ -41,15 +35,10 @@ export function SettingsMobileGridNav({ type="button" onClick={() => onSectionChange(section.id)} className={cn( - "relative flex cursor-pointer flex-col gap-3 rounded-xl border bg-card p-4 text-left transition-colors hover:border-primary/40 active:scale-[0.98]", + "flex cursor-pointer flex-col gap-3 rounded-xl border bg-card p-4 text-left transition-colors hover:border-primary/40 active:scale-[0.98]", section.isDanger && "border-destructive/40" )} > - {section.isComplete && ( -
- -
- )}
-
+

{title}

{description}

diff --git a/app/components/league/settings/SportsSection.tsx b/app/components/league/settings/SportsSection.tsx index fde375e..8a04fc7 100644 --- a/app/components/league/settings/SportsSection.tsx +++ b/app/components/league/settings/SportsSection.tsx @@ -72,7 +72,7 @@ export function SportsSection({ disabled={!canEditSports} /> - + {ss.sport.slug === BRACKT_SLUG ? "Brackt" : `${ss.sport.name} - ${ss.name} (${ss.year})`} diff --git a/app/components/ui/FlagSvg.tsx b/app/components/ui/FlagSvg.tsx index eafa649..75fb263 100644 --- a/app/components/ui/FlagSvg.tsx +++ b/app/components/ui/FlagSvg.tsx @@ -1,5 +1,5 @@ import { cn } from "~/lib/utils"; -import type { FlagConfig } from "~/lib/flag-types"; +import { parseFlagConfig, type FlagConfig } from "~/lib/flag-types"; interface FlagSvgProps { config: FlagConfig; @@ -8,8 +8,11 @@ interface FlagSvgProps { title?: string; } +const FALLBACK_FLAG: FlagConfig = { pattern: "triband-h", colors: ["#9ca3af", "#6b7280", "#4b5563"] }; + export function FlagSvg({ config, size = 48, className, title }: FlagSvgProps) { - const [c0 = "", c1 = "", c2 = ""] = config.colors; + const safeConfig = parseFlagConfig(config) ?? FALLBACK_FLAG; + const [c0, c1, c2] = safeConfig.colors; return ( {title ? {title} : null} - {config.pattern === "triband-h" && ( + {safeConfig.pattern === "triband-h" && ( <> )} - {config.pattern === "triband-v" && ( + {safeConfig.pattern === "triband-v" && ( <> )} - {config.pattern === "diagonal" && ( + {safeConfig.pattern === "diagonal" && ( <> )} - {config.pattern === "nordic-cross" && ( + {safeConfig.pattern === "nordic-cross" && ( <> @@ -51,7 +54,7 @@ export function FlagSvg({ config, size = 48, className, title }: FlagSvgProps) { )} - {config.pattern === "cross" && ( + {safeConfig.pattern === "cross" && ( <> @@ -60,28 +63,28 @@ export function FlagSvg({ config, size = 48, className, title }: FlagSvgProps) { )} - {config.pattern === "canton" && ( + {safeConfig.pattern === "canton" && ( <> )} - {config.pattern === "triangle" && ( + {safeConfig.pattern === "triangle" && ( <> )} - {config.pattern === "chevron" && ( + {safeConfig.pattern === "chevron" && ( <> )} - {config.pattern === "quartered" && ( + {safeConfig.pattern === "quartered" && ( <> @@ -89,21 +92,21 @@ export function FlagSvg({ config, size = 48, className, title }: FlagSvgProps) { )} - {config.pattern === "bordered" && ( + {safeConfig.pattern === "bordered" && ( <> )} - {config.pattern === "circle" && ( + {safeConfig.pattern === "circle" && ( <> )} - {config.pattern === "star" && ( + {safeConfig.pattern === "star" && ( <> { - await db - .update(schema.users) - .set({ flagConfig: generateFlagConfig(user.id) }) - .where(eq(schema.users.id, user.id)); + before: async (data) => { + return { + data: { + ...data, + flagConfig: generateFlagConfig(data.id), + } as typeof data, + }; }, }, update: { @@ -63,7 +65,7 @@ export const auth = betterAuth({ }, advanced: { database: { - generateId: false, + generateId: () => crypto.randomUUID(), }, }, user: { diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index 7f77288..cdc82da 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -307,16 +307,15 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone }; const sectionGridData: SettingsGridSection[] = [ - { id: "league-basics", label: "League Basics", icon: Shield, subtitle: "Name, teams, join", isComplete: true }, - { id: "draft-setup", label: "Draft Settings", icon: Swords, subtitle: "Format, timing, rounds", isComplete: !!(draftDate && draftTime) }, - { id: "draft-order", label: "Draft Order", icon: ListOrdered, subtitle: "Pick order, randomize", isComplete: draftOrderSet }, - { id: "sports", label: "Sports", icon: Trophy, subtitle: `${selectedSports.size} season${selectedSports.size !== 1 ? "s" : ""} selected`, isComplete: selectedSports.size > 0 }, - { id: "scoring", label: "Scoring", icon: TrendingUp, subtitle: "Place values, ties", isComplete: true }, - { id: "notifications", label: "Notifications", icon: Bell, subtitle: "Discord alerts", isComplete: !!league.discordWebhookUrl }, - { id: "people", label: "People", icon: Users, subtitle: `${teamsWithOwners} of ${teamCountValue} teams`, isComplete: false }, - { id: "history-danger", label: "History & Danger", icon: AlertTriangle, subtitle: "Audit log, dissolve", isComplete: false, isDanger: true }, + { id: "league-basics", label: "League Basics", icon: Shield, subtitle: "Name, teams, join" }, + { id: "draft-setup", label: "Draft Settings", icon: Swords, subtitle: "Format, timing, rounds" }, + { id: "draft-order", label: "Draft Order", icon: ListOrdered, subtitle: "Pick order, randomize" }, + { id: "sports", label: "Sports", icon: Trophy, subtitle: `${selectedSports.size} season${selectedSports.size !== 1 ? "s" : ""} selected` }, + { id: "scoring", label: "Scoring", icon: TrendingUp, subtitle: "Place values, ties" }, + { id: "notifications", label: "Notifications", icon: Bell, subtitle: "Discord alerts" }, + { id: "people", label: "People", icon: Users, subtitle: `${teamsWithOwners} of ${teamCountValue} teams` }, + { id: "history-danger", label: "History & Danger", icon: AlertTriangle, subtitle: "Audit log, dissolve", isDanger: true }, ]; - const completedSectionCount = sectionGridData.filter((s) => s.isComplete).length; return (
@@ -328,7 +327,6 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
handleSectionChange(id as SettingsSectionId, { mobile: true })} />