From 5db574263fb5b5675f7c41ee60f0036dabd3c1ac Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 01:40:00 +0000 Subject: [PATCH 1/2] Fix group stage default view and localize match times Default to Groups tab instead of Bracket when no teams have been assigned to the knockout bracket yet (group stage still in progress). Fix match kick-off times showing in UTC server timezone by adding suppressHydrationWarning so React uses the client's local timezone during hydration. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_011Bo6Rsmg8FosYcDJsg14tX --- app/components/sport-season/GroupStageStandings.tsx | 4 ++-- .../leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/components/sport-season/GroupStageStandings.tsx b/app/components/sport-season/GroupStageStandings.tsx index 4931914..360b0c0 100644 --- a/app/components/sport-season/GroupStageStandings.tsx +++ b/app/components/sport-season/GroupStageStandings.tsx @@ -70,7 +70,7 @@ function MatchResult({ match }: { match: GroupMatch }) { {p2} {kickoff && ( - + {kickoff.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })} )} @@ -181,7 +181,7 @@ export function GroupStageStandings({
{[...byDay.entries()].map(([day, matches]) => (
-

+

{day}

{matches.map((m) => ( diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx index 6093b88..d69355e 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx @@ -45,6 +45,7 @@ export default function SportSeasonDetail({ } = loaderData; const hasBracket = playoffMatches && playoffMatches.length > 0; + const bracketHasTeams = hasBracket && playoffMatches.some((m) => m.participant1 !== null || m.participant2 !== null); const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0; const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null); @@ -75,7 +76,8 @@ export default function SportSeasonDetail({ const [view, setView] = useState(() => { if (showGroupStageToggle) { - return sportsSeason.status === "completed" ? "finished" : "playoffs"; + if (sportsSeason.status === "completed") return "finished"; + return bracketHasTeams ? "playoffs" : "groups"; } if (!hasBracket) return "standings"; if (sportsSeason.status === "completed") return "finished"; -- 2.45.3 From 4bede9013a16aba4ce0687645ff4dfd8ec517fb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 03:25:32 +0000 Subject: [PATCH 2/2] Fix four code review findings in group stage view 1. Stable byDay map key: use UTC date string (scheduledAt.slice(0,10)) instead of toLocaleDateString so server and client always produce the same Map structure, eliminating the key={day} hydration tree mismatch. 2. Correct time localization: render UTC time as the stable SSR value, then replace with the user's local time in useEffect after hydration. Removes suppressHydrationWarning from both elements. 3. Fix groups vs visibleGroups: the JSX was iterating the unfiltered groups array, making the showEmpty filter a no-op. Now uses visibleGroups.map so groups with no activity are properly hidden. 4. Sync view after revalidation: add useEffect that advances the tab from "groups" to "playoffs" when bracketHasTeams flips true during an active session, without overriding a deliberate manual selection. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_011Bo6Rsmg8FosYcDJsg14tX --- .../sport-season/GroupStageStandings.tsx | 63 +++++++++++++------ ...eagueId.sports-seasons.$sportsSeasonId.tsx | 12 +++- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/app/components/sport-season/GroupStageStandings.tsx b/app/components/sport-season/GroupStageStandings.tsx index 360b0c0..74733e8 100644 --- a/app/components/sport-season/GroupStageStandings.tsx +++ b/app/components/sport-season/GroupStageStandings.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import type { GroupStandingsRow } from "~/models/group-stage-match"; import { TeamOwnerBadge } from "~/components/ui/team-owner-badge"; @@ -38,6 +39,23 @@ function MatchResult({ match }: { match: GroupMatch }) { const p1 = match.participant1?.name ?? "?"; const p2 = match.participant2?.name ?? "?"; + // SSR renders UTC time (stable); useEffect replaces with the user's local time after hydration. + const [kickoffDisplay, setKickoffDisplay] = useState(() => + match.scheduledAt + ? new Date(match.scheduledAt).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + timeZone: "UTC", + }) + : null + ); + useEffect(() => { + if (!match.scheduledAt) return; + setKickoffDisplay( + new Date(match.scheduledAt).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }) + ); + }, [match.scheduledAt]); + if (match.isComplete && match.participant1Score !== null && match.participant2Score !== null) { const s1 = match.participant1Score; const s2 = match.participant2Score; @@ -61,7 +79,6 @@ function MatchResult({ match }: { match: GroupMatch }) { ); } - const kickoff = match.scheduledAt ? new Date(match.scheduledAt) : null; return (
@@ -69,9 +86,9 @@ function MatchResult({ match }: { match: GroupMatch }) { vs {p2}
- {kickoff && ( - - {kickoff.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })} + {kickoffDisplay && ( + + {kickoffDisplay} )}
@@ -93,7 +110,7 @@ export function GroupStageStandings({

Group Stage

- {groups.map((group) => ( + {visibleGroups.map((group) => (
(); for (const m of sorted) { - const key = m.scheduledAt - ? new Date(m.scheduledAt).toLocaleDateString("en-US", { month: "short", day: "numeric" }) - : "TBD"; + const key = m.scheduledAt ? m.scheduledAt.slice(0, 10) : "TBD"; if (!byDay.has(key)) byDay.set(key, []); byDay.get(key)?.push(m); } return (
- {[...byDay.entries()].map(([day, matches]) => ( -
-

- {day} -

- {matches.map((m) => ( - - ))} -
- ))} + {[...byDay.entries()].map(([key, matches]) => { + // Format from the UTC date string so label is identical on server and client. + const dayLabel = key === "TBD" + ? "TBD" + : new Date(key + "T12:00:00Z").toLocaleDateString("en-US", { + month: "short", + day: "numeric", + timeZone: "UTC", + }); + return ( +
+

+ {dayLabel} +

+ {matches.map((m) => ( + + ))} +
+ ); + })}
); })()} diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx index d69355e..2164b83 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId"; import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.server"; @@ -84,6 +84,16 @@ export default function SportSeasonDetail({ return "playoffs"; }); + // When the loader revalidates and bracket teams are seeded, promote the default tab + // from "groups" to "playoffs". Only fires if the view is still at the automatic default + // ("groups"), so a user who manually clicked Groups from Bracket view is unaffected. + useEffect(() => { + if (!showGroupStageToggle || sportsSeason.status === "completed") return; + if (bracketHasTeams) { + setView((v) => (v === "groups" ? "playoffs" : v)); + } + }, [bracketHasTeams, showGroupStageToggle, sportsSeason.status]); + const TOGGLE_VIEWS: { value: BracketView; label: string }[] = showGroupStageToggle ? [ { value: "groups", label: "Groups" }, -- 2.45.3