feat: improve league home standings and sports seasons UX (#65)
- Sort sports seasons by status (active → upcoming → completed), with
season_standings pattern last within active, then alphabetical
- Fix rank display for teams without standings: T1 when no scores exist,
T{n+1} when some teams are scored (avoids misleading T1 for unranked teams)
- Extract rank logic to getDisplayRank() in standings-display.ts with tests
- Pre-compute standingsMap and sortedTeams to eliminate O(n²) render lookups
- Fix invite URL SSR flash by deriving origin in the loader
- Guard toast useEffect with processedParamsRef to prevent double-fire
- Hide Teams Filled and Draft Order Set post-draft; show Standings sidebar
link only when active/completed
- Show "Final standings" label when season is completed
- Remove duplicate Season Status from sidebar; add Flex Spots description
- Fix status type cast to occur at the model mapping layer, not in JSX
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8c707d2e79
commit
b0960c3c30
4 changed files with 189 additions and 92 deletions
40
app/lib/__tests__/standings-display.test.ts
Normal file
40
app/lib/__tests__/standings-display.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { getDisplayRank } from "../standings-display";
|
||||||
|
|
||||||
|
describe("getDisplayRank", () => {
|
||||||
|
describe("when the team has a standing", () => {
|
||||||
|
it("returns the numeric rank regardless of how many other teams have standings", () => {
|
||||||
|
expect(getDisplayRank({ currentRank: 1 }, 5)).toBe(1);
|
||||||
|
expect(getDisplayRank({ currentRank: 3 }, 5)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns rank 1 when the team is first", () => {
|
||||||
|
expect(getDisplayRank({ currentRank: 1 }, 1)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the team has no standing and no teams have been scored yet", () => {
|
||||||
|
it("returns T1 — all teams are genuinely tied at zero", () => {
|
||||||
|
expect(getDisplayRank(undefined, 0)).toBe("T1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the team has no standing but other teams have been scored", () => {
|
||||||
|
it("returns T{n+1} where n is the number of teams with standings", () => {
|
||||||
|
// 1 team has a standing → unranked teams are tied for 2nd
|
||||||
|
expect(getDisplayRank(undefined, 1)).toBe("T2");
|
||||||
|
|
||||||
|
// 3 teams have standings → unranked teams are tied for 4th
|
||||||
|
expect(getDisplayRank(undefined, 3)).toBe("T4");
|
||||||
|
|
||||||
|
// 9 teams have standings → unranked team is tied for 10th
|
||||||
|
expect(getDisplayRank(undefined, 9)).toBe("T10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never shows T1 when any other team has a standing", () => {
|
||||||
|
// Would be wrong to show T1 here — that team isn't first
|
||||||
|
expect(getDisplayRank(undefined, 1)).not.toBe("T1");
|
||||||
|
expect(getDisplayRank(undefined, 5)).not.toBe("T1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
20
app/lib/standings-display.ts
Normal file
20
app/lib/standings-display.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Returns the display rank for a team on the league home standings preview.
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - If the team has a standing, return its numeric rank.
|
||||||
|
* - If the team has no standing, it is tied at the position just after all
|
||||||
|
* ranked teams. For example, if 3 teams have standings the unranked teams
|
||||||
|
* show "T4". If no team has standings yet (scores haven't been calculated)
|
||||||
|
* every team is genuinely tied at zero and shows "T1".
|
||||||
|
*
|
||||||
|
* @param standing - The team's standing record, or undefined if none exists.
|
||||||
|
* @param standingsCount - Total number of teams that have a standing record.
|
||||||
|
*/
|
||||||
|
export function getDisplayRank(
|
||||||
|
standing: { currentRank: number } | undefined,
|
||||||
|
standingsCount: number
|
||||||
|
): number | string {
|
||||||
|
if (standing) return standing.currentRank;
|
||||||
|
return `T${standingsCount + 1}`;
|
||||||
|
}
|
||||||
|
|
@ -115,7 +115,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
const sportsSeasons = seasonWithSports?.seasonSports?.map((ss) => ({
|
const sportsSeasons = seasonWithSports?.seasonSports?.map((ss) => ({
|
||||||
id: ss.sportsSeason.id,
|
id: ss.sportsSeason.id,
|
||||||
name: ss.sportsSeason.name,
|
name: ss.sportsSeason.name,
|
||||||
status: ss.sportsSeason.status,
|
status: ss.sportsSeason.status as "upcoming" | "active" | "completed",
|
||||||
scoringPattern: ss.sportsSeason.scoringPattern,
|
scoringPattern: ss.sportsSeason.scoringPattern,
|
||||||
sport: ss.sportsSeason.sport,
|
sport: ss.sportsSeason.sport,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
@ -124,6 +124,9 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
// Check if draft order is set
|
// Check if draft order is set
|
||||||
const isDraftOrderSet = draftSlots.length > 0;
|
const isDraftOrderSet = draftSlots.length > 0;
|
||||||
|
|
||||||
|
// Extract origin for client use (avoids SSR/client mismatch on invite URLs)
|
||||||
|
const origin = new URL(args.request.url).origin;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
league,
|
league,
|
||||||
season,
|
season,
|
||||||
|
|
@ -139,5 +142,6 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
isDraftOrderSet,
|
isDraftOrderSet,
|
||||||
sportsSeasons,
|
sportsSeasons,
|
||||||
standings,
|
standings,
|
||||||
|
origin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link, useSearchParams } from "react-router";
|
import { Link, useSearchParams } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
import { SportSeasonCard } from "~/components/sports/SportSeasonCard";
|
import { SportSeasonCard } from "~/components/sports/SportSeasonCard";
|
||||||
|
import { getDisplayRank } from "~/lib/standings-display";
|
||||||
|
|
||||||
export { loader } from "./$leagueId.server";
|
export { loader } from "./$leagueId.server";
|
||||||
|
|
||||||
|
|
@ -31,21 +32,29 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
isDraftOrderSet,
|
isDraftOrderSet,
|
||||||
sportsSeasons,
|
sportsSeasons,
|
||||||
standings,
|
standings,
|
||||||
|
origin,
|
||||||
} = loaderData;
|
} = loaderData;
|
||||||
|
|
||||||
const myTeam = teams.find((t) => t.ownerId === currentUserId);
|
const myTeam = teams.find((t) => t.ownerId === currentUserId);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
// Guard against double-firing toasts (e.g. React Strict Mode double-invoke)
|
||||||
|
const processedParamsRef = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const paramString = searchParams.toString();
|
||||||
|
if (!paramString || paramString === processedParamsRef.current) return;
|
||||||
|
|
||||||
if (searchParams.get("updated") === "true") {
|
if (searchParams.get("updated") === "true") {
|
||||||
|
processedParamsRef.current = paramString;
|
||||||
toast.success("Team updated successfully");
|
toast.success("Team updated successfully");
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
}
|
} else if (searchParams.get("joined") === "true") {
|
||||||
if (searchParams.get("joined") === "true") {
|
processedParamsRef.current = paramString;
|
||||||
toast.success("Welcome to the league! You've been assigned a team.");
|
toast.success("Welcome to the league! You've been assigned a team.");
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
}
|
} else if (searchParams.get("left") === "true") {
|
||||||
if (searchParams.get("left") === "true") {
|
processedParamsRef.current = paramString;
|
||||||
toast.success("You have left the league. Your team is now available.");
|
toast.success("You have left the league. Your team is now available.");
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +63,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
const handleCopyInviteLink = async () => {
|
const handleCopyInviteLink = async () => {
|
||||||
if (!season?.inviteCode) return;
|
if (!season?.inviteCode) return;
|
||||||
|
|
||||||
const inviteUrl = `${window.location.origin}/i/${season.inviteCode}`;
|
const inviteUrl = `${origin}/i/${season.inviteCode}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(inviteUrl);
|
await navigator.clipboard.writeText(inviteUrl);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
|
|
@ -65,6 +74,33 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pre-compute standings map to avoid O(n²) lookups in render
|
||||||
|
const standingsMap = new Map(standings.map((s) => [s.teamId, s]));
|
||||||
|
|
||||||
|
// Pre-compute sorted standings list
|
||||||
|
const sortedTeams = [...teams].sort((a, b) => {
|
||||||
|
const rankA = standingsMap.get(a.id)?.currentRank ?? Infinity;
|
||||||
|
const rankB = standingsMap.get(b.id)?.currentRank ?? Infinity;
|
||||||
|
return rankA - rankB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-compute sorted sports seasons: active → upcoming → completed,
|
||||||
|
// season_standings last within active, then alphabetical by sport name
|
||||||
|
const sortedSportsSeasons = [...sportsSeasons].sort((a, b) => {
|
||||||
|
const statusOrder = { active: 0, upcoming: 1, completed: 2 } as const;
|
||||||
|
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
||||||
|
if (statusDiff !== 0) return statusDiff;
|
||||||
|
if (a.status === "active") {
|
||||||
|
const aSeasonLong = a.scoringPattern === "season_standings" ? 1 : 0;
|
||||||
|
const bSeasonLong = b.scoringPattern === "season_standings" ? 1 : 0;
|
||||||
|
if (aSeasonLong !== bSeasonLong) return aSeasonLong - bSeasonLong;
|
||||||
|
}
|
||||||
|
return a.sport.name.localeCompare(b.sport.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDraftOrPreDraft = season?.status === "pre_draft" || season?.status === "draft";
|
||||||
|
const isActiveOrCompleted = season?.status === "active" || season?.status === "completed";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4">
|
<div className="container mx-auto py-8 px-4">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|
@ -89,7 +125,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</div>
|
</div>
|
||||||
{season && (
|
{season && (
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{season.year} Season • Status:{" "}
|
{season.year} Season •{" "}
|
||||||
<span className="capitalize">
|
<span className="capitalize">
|
||||||
{season.status.replace("_", " ")}
|
{season.status.replace("_", " ")}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -121,13 +157,15 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
{/* Left Column - 2/3 width on desktop */}
|
{/* Left Column - 2/3 width on desktop */}
|
||||||
<div className="md:col-span-2 space-y-6">
|
<div className="md:col-span-2 space-y-6">
|
||||||
{/* Standings Panel - active/completed seasons */}
|
{/* Standings Panel - active/completed seasons */}
|
||||||
{season && (season.status === "active" || season.status === "completed") && teams.length > 0 && (
|
{season && isActiveOrCompleted && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Standings</CardTitle>
|
<CardTitle>Standings</CardTitle>
|
||||||
<CardDescription>Current season standings</CardDescription>
|
<CardDescription>
|
||||||
|
{season.status === "completed" ? "Final standings" : "Current season standings"}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link to={`/leagues/${league.id}/standings/${season.id}`}>
|
<Link to={`/leagues/${league.id}/standings/${season.id}`}>
|
||||||
|
|
@ -138,12 +176,8 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{[...teams].sort((a, b) => {
|
{sortedTeams.map((team) => {
|
||||||
const rankA = standings.find((s) => s.teamId === a.id)?.currentRank ?? Infinity;
|
const standing = standingsMap.get(team.id);
|
||||||
const rankB = standings.find((s) => s.teamId === b.id)?.currentRank ?? Infinity;
|
|
||||||
return rankA - rankB;
|
|
||||||
}).map((team) => {
|
|
||||||
const standing = standings.find((s) => s.teamId === team.id);
|
|
||||||
const ownerName = team.ownerId ? ownerMap[team.ownerId] : null;
|
const ownerName = team.ownerId ? ownerMap[team.ownerId] : null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -151,7 +185,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
className="flex items-center gap-3 py-2 border-b last:border-0"
|
className="flex items-center gap-3 py-2 border-b last:border-0"
|
||||||
>
|
>
|
||||||
<div className="w-8 text-center text-sm font-bold text-muted-foreground">
|
<div className="w-8 text-center text-sm font-bold text-muted-foreground">
|
||||||
{standing ? standing.currentRank : "T1"}
|
{getDisplayRank(standing, standings.length)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -178,24 +212,24 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sports Seasons Section */}
|
{/* Sports Seasons Section */}
|
||||||
{season && sportsSeasons.length > 0 && (
|
{season && sortedSportsSeasons.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sports Seasons</CardTitle>
|
<CardTitle>Sports Seasons</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{sportsSeasons.length} sport{sportsSeasons.length !== 1 ? "s" : ""} in this season
|
{sortedSportsSeasons.length} sport{sortedSportsSeasons.length !== 1 ? "s" : ""} in this season
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{sportsSeasons.map((sportSeason) => (
|
{sortedSportsSeasons.map((sportSeason) => (
|
||||||
<SportSeasonCard
|
<SportSeasonCard
|
||||||
key={sportSeason.id}
|
key={sportSeason.id}
|
||||||
leagueId={league.id}
|
leagueId={league.id}
|
||||||
sportSeasonId={sportSeason.id}
|
sportSeasonId={sportSeason.id}
|
||||||
sportName={sportSeason.sport.name}
|
sportName={sportSeason.sport.name}
|
||||||
seasonName={sportSeason.name}
|
seasonName={sportSeason.name}
|
||||||
status={sportSeason.status as "upcoming" | "active" | "completed"}
|
status={sportSeason.status}
|
||||||
scoringPattern={sportSeason.scoringPattern}
|
scoringPattern={sportSeason.scoringPattern}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -219,7 +253,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
value={`${typeof window !== "undefined" ? window.location.origin : ""}/i/${season.inviteCode}`}
|
value={`${origin}/i/${season.inviteCode}`}
|
||||||
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted"
|
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted"
|
||||||
onClick={(e) => e.currentTarget.select()}
|
onClick={(e) => e.currentTarget.select()}
|
||||||
/>
|
/>
|
||||||
|
|
@ -239,55 +273,55 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Teams card - pre_draft/draft only; replaced by standings panel post-draft */}
|
{/* Teams card - pre_draft/draft only; replaced by standings panel post-draft */}
|
||||||
{season && (season.status === "pre_draft" || season.status === "draft") && <Card>
|
{season && isDraftOrPreDraft && (
|
||||||
<CardHeader>
|
<Card>
|
||||||
<CardTitle>Teams</CardTitle>
|
<CardHeader>
|
||||||
<CardDescription>
|
<CardTitle>Teams</CardTitle>
|
||||||
{teams.length} team{teams.length !== 1 ? "s" : ""} in this
|
<CardDescription>
|
||||||
league
|
{teams.length} team{teams.length !== 1 ? "s" : ""} in this league
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{teams.map((team) => {
|
{teams.map((team) => {
|
||||||
const isOwned = team.ownerId === currentUserId;
|
const isOwned = team.ownerId === currentUserId;
|
||||||
const ownerName = team.ownerId
|
const ownerName = team.ownerId
|
||||||
? ownerMap[team.ownerId]
|
? ownerMap[team.ownerId]
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={team.id}
|
key={team.id}
|
||||||
className={isOwned ? "border-primary" : ""}
|
className={isOwned ? "border-primary" : ""}
|
||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">{team.name}</CardTitle>
|
<CardTitle className="text-lg">{team.name}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{team.ownerId ? (
|
{team.ownerId ? (
|
||||||
isOwned ? (
|
isOwned ? (
|
||||||
<span className="text-primary font-medium">
|
<span className="text-primary font-medium">
|
||||||
Your Team
|
Your Team
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{ownerName || "Unknown Owner"}</span>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<span>{ownerName || "Unknown Owner"}</span>
|
<span className="text-muted-foreground">
|
||||||
)
|
Available
|
||||||
) : (
|
</span>
|
||||||
<span className="text-muted-foreground">
|
)}
|
||||||
Available
|
</CardDescription>
|
||||||
</span>
|
</CardHeader>
|
||||||
)}
|
</Card>
|
||||||
</CardDescription>
|
);
|
||||||
</CardHeader>
|
})}
|
||||||
</Card>
|
</div>
|
||||||
);
|
</CardContent>
|
||||||
})}
|
</Card>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
|
||||||
</Card>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - 1/3 width on desktop */}
|
{/* Right Column - 1/3 width on desktop */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>League Info</CardTitle>
|
<CardTitle>League Info</CardTitle>
|
||||||
|
|
@ -301,23 +335,17 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</div>
|
</div>
|
||||||
{season && (
|
{season && (
|
||||||
<>
|
<>
|
||||||
<div>
|
{isDraftOrPreDraft && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<div>
|
||||||
Season Status
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
Teams Filled
|
||||||
<p className="font-medium capitalize">
|
</p>
|
||||||
{season.status.replace("_", " ")}
|
<p className="font-medium">
|
||||||
</p>
|
{teamsWithOwners} of {teams.length}
|
||||||
</div>
|
</p>
|
||||||
<div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
)}
|
||||||
Teams Filled
|
{isDraftOrPreDraft && (
|
||||||
</p>
|
|
||||||
<p className="font-medium">
|
|
||||||
{teamsWithOwners} of {teams.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{(season.status === "pre_draft" || season.status === "draft") && (
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Draft Order Set
|
Draft Order Set
|
||||||
|
|
@ -357,20 +385,25 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
View Draft Board
|
View Draft Board
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{isActiveOrCompleted && (
|
||||||
<p className="text-sm text-muted-foreground">Standings</p>
|
<div>
|
||||||
<Link
|
<p className="text-sm text-muted-foreground">Standings</p>
|
||||||
to={`/leagues/${league.id}/standings/${season.id}`}
|
<Link
|
||||||
className="text-electric hover:underline font-medium"
|
to={`/leagues/${league.id}/standings/${season.id}`}
|
||||||
>
|
className="text-electric hover:underline font-medium"
|
||||||
View Standings
|
>
|
||||||
</Link>
|
View Standings
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Flex Spots</p>
|
<p className="text-sm text-muted-foreground">Flex Spots</p>
|
||||||
<p className="font-medium text-primary">
|
<p className="font-medium text-primary">
|
||||||
{Math.max(0, season.draftRounds - sportsCount)}
|
{Math.max(0, season.draftRounds - sportsCount)}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Picks not tied to a specific sport
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{season.draftDateTime && (
|
{season.draftDateTime && (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue