Simplify sports season display and add local timezone hints (#434)
* Show season name instead of year/bracket format; add local timezone hint to draft time Replace the raw year number and scoringType string in the league creation sport picker, review step, and league settings sports section with the human-readable season name (e.g. "2025 NBA Season"), matching how the league homepage sports list already displays seasons. Add a client-side local timezone label below the draft date & time fields on both the league creation wizard and the league settings page, so users know that draft times are in their local timezone. https://claude.ai/code/session_01GtJowGruAc1A4jURkSDVjX * Address code review: shared tz hook, short abbrev, sort fix, dead type cleanup, test update - Extract timezone detection into useLocalTimezone hook to avoid duplication across DraftSetupSection and Step3DraftSettings - Use Intl short timezone abbreviation (e.g. "EST") instead of raw IANA name, matching DraftInfoCard's existing pattern - Sort review step sport list by season name instead of sport.name so the visible order matches the displayed text - Remove unused scoringType and year fields from SportsSection's local type - Update SportsSection tests to use realistic season names and the new display format https://claude.ai/code/session_01GtJowGruAc1A4jURkSDVjX --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d92f73e449
commit
55828e9f42
5 changed files with 28 additions and 19 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { Swords, CalendarIcon } from "lucide-react";
|
import { Swords, CalendarIcon } from "lucide-react";
|
||||||
|
import { useLocalTimezone } from "~/hooks/useLocalTimezone";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
|
|
@ -64,6 +65,8 @@ export function DraftSetupSection({
|
||||||
onOvernightTimezoneChange: (v: string) => void;
|
onOvernightTimezoneChange: (v: string) => void;
|
||||||
commishTimezone: string | null;
|
commishTimezone: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const localTz = useLocalTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
id="draft-setup"
|
id="draft-setup"
|
||||||
|
|
@ -145,6 +148,7 @@ export function DraftSetupSection({
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-sm text-muted-foreground">
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
Set this before starting the draft room.
|
Set this before starting the draft room.
|
||||||
|
{localTz && <> Times are in your local timezone ({localTz}).</>}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import { SettingsSection, SettingsStatusPill } from "./SettingsSection";
|
||||||
type SportsSeason = {
|
type SportsSeason = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
year: number;
|
|
||||||
scoringType: string;
|
|
||||||
sport: { id: string; name: string; slug: string; type: string; iconUrl: string | null };
|
sport: { id: string; name: string; slug: string; type: string; iconUrl: string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -75,9 +73,8 @@ export function SportsSection({
|
||||||
<SportIcon sport={ss.sport} />
|
<SportIcon sport={ss.sport} />
|
||||||
<span className="min-w-0 flex-1 overflow-hidden">
|
<span className="min-w-0 flex-1 overflow-hidden">
|
||||||
<span className="block truncate font-medium">
|
<span className="block truncate font-medium">
|
||||||
{ss.sport.slug === BRACKT_SLUG ? "Brackt" : `${ss.sport.name} - ${ss.name} (${ss.year})`}
|
{ss.sport.slug === BRACKT_SLUG ? "Brackt" : ss.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">{ss.scoringType.replace("_", " ")}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,18 @@ import { SportsSection } from "../SportsSection";
|
||||||
type SportsSeason = {
|
type SportsSeason = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
year: number;
|
|
||||||
scoringType: string;
|
|
||||||
sport: { id: string; name: string; slug: string; type: string; iconUrl: string | null };
|
sport: { id: string; name: string; slug: string; type: string; iconUrl: string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
const displaySportsSeasons: SportsSeason[] = [
|
const displaySportsSeasons: SportsSeason[] = [
|
||||||
{
|
{
|
||||||
id: "nba-2025",
|
id: "nba-2025",
|
||||||
name: "2025 Season",
|
name: "2025 NBA Season",
|
||||||
year: 2025,
|
|
||||||
scoringType: "standings",
|
|
||||||
sport: { id: "sport-1", name: "NBA", slug: "nba", type: "team", iconUrl: "/sports-icons/nba.svg" },
|
sport: { id: "sport-1", name: "NBA", slug: "nba", type: "team", iconUrl: "/sports-icons/nba.svg" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mlb-2025",
|
id: "mlb-2025",
|
||||||
name: "2025 Season",
|
name: "2025 MLB Season",
|
||||||
year: 2025,
|
|
||||||
scoringType: "playoffs",
|
|
||||||
sport: { id: "sport-2", name: "MLB", slug: "mlb", type: "team", iconUrl: "/sports-icons/mlb.svg" },
|
sport: { id: "sport-2", name: "MLB", slug: "mlb", type: "team", iconUrl: "/sports-icons/mlb.svg" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -82,10 +76,10 @@ describe("SportsSection", () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<SportsSectionHarness />);
|
render(<SportsSectionHarness />);
|
||||||
|
|
||||||
const mlbCheckbox = screen.getByRole("checkbox", { name: /mlb - 2025 season \(2025\)/i });
|
const mlbCheckbox = screen.getByRole("checkbox", { name: /2025 mlb season/i });
|
||||||
expect(mlbCheckbox).not.toBeChecked();
|
expect(mlbCheckbox).not.toBeChecked();
|
||||||
|
|
||||||
await user.click(screen.getByText("MLB - 2025 Season (2025)"));
|
await user.click(screen.getByText("2025 MLB Season"));
|
||||||
|
|
||||||
expect(mlbCheckbox).toBeChecked();
|
expect(mlbCheckbox).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
@ -95,7 +89,7 @@ describe("SportsSection", () => {
|
||||||
const handleSubmit = vi.fn();
|
const handleSubmit = vi.fn();
|
||||||
render(<SportsSectionHarness onSubmit={handleSubmit} />);
|
render(<SportsSectionHarness onSubmit={handleSubmit} />);
|
||||||
|
|
||||||
await user.click(screen.getByText("MLB - 2025 Season (2025)"));
|
await user.click(screen.getByText("2025 MLB Season"));
|
||||||
await user.click(screen.getByRole("button", { name: "Save" }));
|
await user.click(screen.getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
expect(handleSubmit).toHaveBeenCalledWith(["nba-2025", "mlb-2025"]);
|
expect(handleSubmit).toHaveBeenCalledWith(["nba-2025", "mlb-2025"]);
|
||||||
|
|
|
||||||
10
app/hooks/useLocalTimezone.ts
Normal file
10
app/hooks/useLocalTimezone.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useLocalTimezone() {
|
||||||
|
const [tz, setTz] = useState("");
|
||||||
|
useEffect(() => {
|
||||||
|
const parts = Intl.DateTimeFormat(undefined, { timeZoneName: "short" }).formatToParts(new Date());
|
||||||
|
setTz(parts.find((p) => p.type === "timeZoneName")?.value ?? "");
|
||||||
|
}, []);
|
||||||
|
return tz;
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import { Checkbox } from "~/components/ui/checkbox";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { parseDraftSpeed, formatTime12h } from "~/lib/draft-timer";
|
import { parseDraftSpeed, formatTime12h } from "~/lib/draft-timer";
|
||||||
import { TIMEZONE_OPTIONS } from "~/components/league/TimezoneSelect";
|
import { TIMEZONE_OPTIONS } from "~/components/league/TimezoneSelect";
|
||||||
|
import { useLocalTimezone } from "~/hooks/useLocalTimezone";
|
||||||
import { saveWizardState, buildFormDataFromSnapshot } from "~/lib/wizard-state";
|
import { saveWizardState, buildFormDataFromSnapshot } from "~/lib/wizard-state";
|
||||||
import { type ScoringRules, DEFAULT_SCORING_RULES } from "~/lib/scoring-types";
|
import { type ScoringRules, DEFAULT_SCORING_RULES } from "~/lib/scoring-types";
|
||||||
import { SportIcon } from "~/components/league/SportIcon";
|
import { SportIcon } from "~/components/league/SportIcon";
|
||||||
|
|
@ -533,7 +534,7 @@ function Step2Sports({
|
||||||
className="inline-flex items-center gap-1 pl-2 pr-1 py-0.5 rounded-md bg-primary/10 border border-primary text-primary text-sm font-medium"
|
className="inline-flex items-center gap-1 pl-2 pr-1 py-0.5 rounded-md bg-primary/10 border border-primary text-primary text-sm font-medium"
|
||||||
>
|
>
|
||||||
<SportIcon sport={s.sport} />
|
<SportIcon sport={s.sport} />
|
||||||
{s.sport.name} <span className="text-xs opacity-70">{s.year}</span>
|
{s.name}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleSport(s.id)}
|
onClick={() => toggleSport(s.id)}
|
||||||
|
|
@ -568,7 +569,7 @@ function Step2Sports({
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<SportIcon sport={s.sport} />
|
<SportIcon sport={s.sport} />
|
||||||
{s.sport.name} <span className="text-xs opacity-70">{s.year}</span>
|
{s.name}
|
||||||
{isBracktSport(s.sport) && <BracktInfoIcon />}
|
{isBracktSport(s.sport) && <BracktInfoIcon />}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-semibold">{selected ? "Remove" : "Add"}</span>
|
<span className="text-xs font-semibold">{selected ? "Remove" : "Add"}</span>
|
||||||
|
|
@ -659,6 +660,8 @@ function Step3DraftSettings({
|
||||||
const { draftInitialTime, draftIncrementTime } = parseDraftSpeed(draftSpeed, timerMode);
|
const { draftInitialTime, draftIncrementTime } = parseDraftSpeed(draftSpeed, timerMode);
|
||||||
const showOvernightPause = draftInitialTime >= 3600 || draftIncrementTime >= 600;
|
const showOvernightPause = draftInitialTime >= 3600 || draftIncrementTime >= 600;
|
||||||
|
|
||||||
|
const localTz = useLocalTimezone();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showOvernightPause) setOvernightMode("none");
|
if (!showOvernightPause) setOvernightMode("none");
|
||||||
}, [showOvernightPause]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [showOvernightPause]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -708,6 +711,7 @@ function Step3DraftSettings({
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
This can be changed before the draft starts.
|
This can be changed before the draft starts.
|
||||||
|
{localTz && <> Times are in your local timezone ({localTz}).</>}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -911,10 +915,10 @@ function StepReview({
|
||||||
<span className="text-primary font-semibold">{uniqueSportsCount} sport{uniqueSportsCount !== 1 ? "s" : ""}</span>
|
<span className="text-primary font-semibold">{uniqueSportsCount} sport{uniqueSportsCount !== 1 ? "s" : ""}</span>
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-1 space-y-1">
|
<ul className="mt-1 space-y-1">
|
||||||
{selectedSeasons.toSorted((a, b) => a.sport.name.localeCompare(b.sport.name)).map((s) => (
|
{selectedSeasons.toSorted((a, b) => a.name.localeCompare(b.name)).map((s) => (
|
||||||
<li key={s.id} className="flex items-center gap-2 text-sm text-foreground">
|
<li key={s.id} className="flex items-center gap-2 text-sm text-foreground">
|
||||||
<SportIcon sport={s.sport} />
|
<SportIcon sport={s.sport} />
|
||||||
{s.sport.name} {s.year}
|
{s.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue