diff --git a/app/components/sport-season/GroupStageStandings.tsx b/app/components/sport-season/GroupStageStandings.tsx index 4931914..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 && ( + {kickoffDisplay && ( - {kickoff.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })} + {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 6093b88..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"; @@ -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,13 +76,24 @@ 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"; 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" },