2026-03-07 15:57:51 -08:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
2025-10-11 01:03:31 -07:00
|
|
|
import { Link, useSearchParams } from "react-router";
|
|
|
|
|
import { toast } from "sonner";
|
2025-10-15 22:02:21 -07:00
|
|
|
import { format } from "date-fns";
|
2025-10-11 00:07:39 -07:00
|
|
|
import type { Route } from "./+types/$leagueId";
|
2025-10-11 01:03:31 -07:00
|
|
|
import { Button } from "~/components/ui/button";
|
2025-10-11 00:07:39 -07:00
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from "~/components/ui/card";
|
2025-11-14 20:39:51 -08:00
|
|
|
import { SportSeasonCard } from "~/components/sports/SportSeasonCard";
|
2026-03-07 15:57:51 -08:00
|
|
|
import { getDisplayRank } from "~/lib/standings-display";
|
2025-10-11 00:07:39 -07:00
|
|
|
|
2025-11-14 21:18:34 -08:00
|
|
|
export { loader } from "./$leagueId.server";
|
2025-10-11 00:07:39 -07:00
|
|
|
|
|
|
|
|
export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
2025-10-14 21:24:58 -07:00
|
|
|
const {
|
|
|
|
|
league,
|
|
|
|
|
season,
|
|
|
|
|
teams,
|
|
|
|
|
commissioners,
|
|
|
|
|
currentUserId,
|
2025-10-14 21:20:58 -07:00
|
|
|
isUserCommissioner,
|
|
|
|
|
ownerMap,
|
|
|
|
|
commissionerMap,
|
|
|
|
|
availableTeamCount,
|
2025-10-15 21:50:02 -07:00
|
|
|
sportsCount,
|
2025-10-21 21:36:42 -07:00
|
|
|
teamsWithOwners,
|
|
|
|
|
isDraftOrderSet,
|
2025-11-14 20:39:51 -08:00
|
|
|
sportsSeasons,
|
2026-03-05 10:07:15 -08:00
|
|
|
standings,
|
2026-03-07 15:57:51 -08:00
|
|
|
origin,
|
2025-10-14 21:20:58 -07:00
|
|
|
} = loaderData;
|
2026-03-07 15:57:51 -08:00
|
|
|
|
2026-03-05 10:07:15 -08:00
|
|
|
const myTeam = teams.find((t) => t.ownerId === currentUserId);
|
2025-10-11 01:03:31 -07:00
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
2025-10-14 12:20:36 -07:00
|
|
|
const [copied, setCopied] = useState(false);
|
2025-10-11 01:03:31 -07:00
|
|
|
|
2026-03-07 15:57:51 -08:00
|
|
|
// Guard against double-firing toasts (e.g. React Strict Mode double-invoke)
|
|
|
|
|
const processedParamsRef = useRef<string | null>(null);
|
2025-10-11 01:03:31 -07:00
|
|
|
useEffect(() => {
|
2026-03-07 15:57:51 -08:00
|
|
|
const paramString = searchParams.toString();
|
|
|
|
|
if (!paramString || paramString === processedParamsRef.current) return;
|
|
|
|
|
|
2025-10-11 01:03:31 -07:00
|
|
|
if (searchParams.get("updated") === "true") {
|
2026-03-07 15:57:51 -08:00
|
|
|
processedParamsRef.current = paramString;
|
2025-10-14 22:04:37 -07:00
|
|
|
toast.success("Team updated successfully");
|
2025-10-14 12:20:36 -07:00
|
|
|
setSearchParams({});
|
2026-03-07 15:57:51 -08:00
|
|
|
} else if (searchParams.get("joined") === "true") {
|
|
|
|
|
processedParamsRef.current = paramString;
|
2025-10-14 12:20:36 -07:00
|
|
|
toast.success("Welcome to the league! You've been assigned a team.");
|
2025-10-11 01:03:31 -07:00
|
|
|
setSearchParams({});
|
2026-03-07 15:57:51 -08:00
|
|
|
} else if (searchParams.get("left") === "true") {
|
|
|
|
|
processedParamsRef.current = paramString;
|
2025-10-14 22:04:37 -07:00
|
|
|
toast.success("You have left the league. Your team is now available.");
|
|
|
|
|
setSearchParams({});
|
|
|
|
|
}
|
2025-10-11 01:03:31 -07:00
|
|
|
}, [searchParams, setSearchParams]);
|
2025-10-11 00:07:39 -07:00
|
|
|
|
2025-10-14 12:20:36 -07:00
|
|
|
const handleCopyInviteLink = async () => {
|
|
|
|
|
if (!season?.inviteCode) return;
|
2025-10-14 21:24:58 -07:00
|
|
|
|
2026-03-07 15:57:51 -08:00
|
|
|
const inviteUrl = `${origin}/i/${season.inviteCode}`;
|
2025-10-14 12:20:36 -07:00
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(inviteUrl);
|
|
|
|
|
setCopied(true);
|
|
|
|
|
toast.success("Invite link copied to clipboard!");
|
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
2025-10-14 21:24:58 -07:00
|
|
|
} catch {
|
2025-10-14 12:20:36 -07:00
|
|
|
toast.error("Failed to copy invite link");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-07 15:57:51 -08:00
|
|
|
// 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";
|
|
|
|
|
|
2025-10-11 00:07:39 -07:00
|
|
|
return (
|
|
|
|
|
<div className="container mx-auto py-8 px-4">
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
|
|
|
<h1 className="text-4xl font-bold">{league.name}</h1>
|
2025-10-11 00:29:04 -07:00
|
|
|
<div className="flex gap-2">
|
2026-03-05 10:07:15 -08:00
|
|
|
{myTeam && (
|
2025-10-14 22:04:37 -07:00
|
|
|
<Button variant="outline" asChild>
|
2026-03-05 10:07:15 -08:00
|
|
|
<Link to={`/teams/${myTeam.id}/settings`}>
|
2025-10-14 22:04:37 -07:00
|
|
|
Team Settings
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-10-11 00:29:04 -07:00
|
|
|
{isUserCommissioner && (
|
|
|
|
|
<Button variant="outline" asChild>
|
2025-10-14 22:04:37 -07:00
|
|
|
<Link to={`/leagues/${league.id}/settings`}>
|
|
|
|
|
League Settings
|
|
|
|
|
</Link>
|
2025-10-11 00:29:04 -07:00
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-11 00:07:39 -07:00
|
|
|
</div>
|
|
|
|
|
{season && (
|
|
|
|
|
<p className="text-muted-foreground">
|
2026-03-07 15:57:51 -08:00
|
|
|
{season.year} Season •{" "}
|
2025-10-11 00:07:39 -07:00
|
|
|
<span className="capitalize">
|
|
|
|
|
{season.status.replace("_", " ")}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-27 23:00:42 -08:00
|
|
|
{season?.status === "draft" && (
|
|
|
|
|
<div className="mb-6 flex items-center justify-between gap-4 rounded-lg border border-yellow-500/50 bg-yellow-500/10 px-4 py-3 text-yellow-700 dark:text-yellow-400">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
|
|
|
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-yellow-500 opacity-75" />
|
|
|
|
|
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-yellow-500" />
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-medium">Draft is live!</span>
|
|
|
|
|
<span className="hidden text-sm sm:inline">
|
|
|
|
|
Your league draft is currently in progress.
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Button asChild size="sm" variant="outline" className="shrink-0 border-yellow-500/60 text-yellow-700 hover:bg-yellow-500/20 dark:text-yellow-400">
|
|
|
|
|
<Link to={`/leagues/${league.id}/draft/${season.id}`}>
|
|
|
|
|
Enter Draft Room
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-10-14 21:54:46 -07:00
|
|
|
<div className="grid gap-6 md:grid-cols-3">
|
|
|
|
|
{/* Left Column - 2/3 width on desktop */}
|
|
|
|
|
<div className="md:col-span-2 space-y-6">
|
2026-03-05 10:07:15 -08:00
|
|
|
{/* Standings Panel - active/completed seasons */}
|
2026-03-07 15:57:51 -08:00
|
|
|
{season && isActiveOrCompleted && (
|
2026-03-05 10:07:15 -08:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle>Standings</CardTitle>
|
2026-03-07 15:57:51 -08:00
|
|
|
<CardDescription>
|
|
|
|
|
{season.status === "completed" ? "Final standings" : "Current season standings"}
|
|
|
|
|
</CardDescription>
|
2026-03-05 10:07:15 -08:00
|
|
|
</div>
|
|
|
|
|
<Button variant="outline" size="sm" asChild>
|
|
|
|
|
<Link to={`/leagues/${league.id}/standings/${season.id}`}>
|
|
|
|
|
Full Standings
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-1">
|
2026-03-07 15:57:51 -08:00
|
|
|
{sortedTeams.map((team) => {
|
|
|
|
|
const standing = standingsMap.get(team.id);
|
2026-03-05 10:07:15 -08:00
|
|
|
const ownerName = team.ownerId ? ownerMap[team.ownerId] : null;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={team.id}
|
|
|
|
|
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">
|
2026-03-07 15:57:51 -08:00
|
|
|
{getDisplayRank(standing, standings.length)}
|
2026-03-05 10:07:15 -08:00
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<Link
|
|
|
|
|
to={`/leagues/${league.id}/standings/${season.id}/teams/${team.id}`}
|
|
|
|
|
className="font-medium text-sm hover:underline truncate block"
|
|
|
|
|
>
|
|
|
|
|
{team.name}
|
|
|
|
|
</Link>
|
|
|
|
|
{ownerName && (
|
|
|
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
|
|
|
{ownerName}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm font-medium tabular-nums">
|
|
|
|
|
{standing ? standing.totalPoints : 0} pts
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-11-14 20:39:51 -08:00
|
|
|
{/* Sports Seasons Section */}
|
2026-03-07 15:57:51 -08:00
|
|
|
{season && sortedSportsSeasons.length > 0 && (
|
2025-11-14 20:39:51 -08:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Sports Seasons</CardTitle>
|
|
|
|
|
<CardDescription>
|
2026-03-07 15:57:51 -08:00
|
|
|
{sortedSportsSeasons.length} sport{sortedSportsSeasons.length !== 1 ? "s" : ""} in this season
|
2025-11-14 20:39:51 -08:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
2026-03-07 15:57:51 -08:00
|
|
|
{sortedSportsSeasons.map((sportSeason) => (
|
2025-11-14 20:39:51 -08:00
|
|
|
<SportSeasonCard
|
|
|
|
|
key={sportSeason.id}
|
|
|
|
|
leagueId={league.id}
|
|
|
|
|
sportSeasonId={sportSeason.id}
|
|
|
|
|
sportName={sportSeason.sport.name}
|
|
|
|
|
seasonName={sportSeason.name}
|
2026-03-07 15:57:51 -08:00
|
|
|
status={sportSeason.status}
|
2025-11-14 20:39:51 -08:00
|
|
|
scoringPattern={sportSeason.scoringPattern}
|
2026-03-07 21:59:29 -08:00
|
|
|
nextEvent={sportSeason.nextEvent}
|
2025-11-14 20:39:51 -08:00
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-05 10:07:15 -08:00
|
|
|
{isUserCommissioner && season && season.status === "pre_draft" && availableTeamCount > 0 && (
|
2025-10-14 21:54:46 -07:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Invite Link</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Share this link to invite people to join your league (
|
|
|
|
|
{availableTeamCount} spot{availableTeamCount !== 1 ? "s" : ""}{" "}
|
|
|
|
|
available)
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
readOnly
|
2026-03-07 15:57:51 -08:00
|
|
|
value={`${origin}/i/${season.inviteCode}`}
|
2025-10-14 21:54:46 -07:00
|
|
|
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted"
|
|
|
|
|
onClick={(e) => e.currentTarget.select()}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleCopyInviteLink}
|
|
|
|
|
variant={copied ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
>
|
|
|
|
|
{copied ? "Copied!" : "Copy"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Anyone with this link can join your league
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-05 10:07:15 -08:00
|
|
|
{/* Teams card - pre_draft/draft only; replaced by standings panel post-draft */}
|
2026-03-07 15:57:51 -08:00
|
|
|
{season && isDraftOrPreDraft && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Teams</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{teams.length} team{teams.length !== 1 ? "s" : ""} in this league
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{teams.map((team) => {
|
|
|
|
|
const isOwned = team.ownerId === currentUserId;
|
|
|
|
|
const ownerName = team.ownerId
|
|
|
|
|
? ownerMap[team.ownerId]
|
|
|
|
|
: null;
|
|
|
|
|
return (
|
|
|
|
|
<Card
|
|
|
|
|
key={team.id}
|
|
|
|
|
className={isOwned ? "border-primary" : ""}
|
|
|
|
|
>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg">{team.name}</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{team.ownerId ? (
|
|
|
|
|
isOwned ? (
|
|
|
|
|
<span className="text-primary font-medium">
|
|
|
|
|
Your Team
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span>{ownerName || "Unknown Owner"}</span>
|
|
|
|
|
)
|
2025-10-14 21:54:46 -07:00
|
|
|
) : (
|
2026-03-07 15:57:51 -08:00
|
|
|
<span className="text-muted-foreground">
|
|
|
|
|
Available
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-10-14 21:54:46 -07:00
|
|
|
</div>
|
2025-10-11 00:07:39 -07:00
|
|
|
|
2025-10-14 21:54:46 -07:00
|
|
|
{/* Right Column - 1/3 width on desktop */}
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>League Info</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-2">
|
2025-10-11 00:07:39 -07:00
|
|
|
<div>
|
2025-10-14 21:54:46 -07:00
|
|
|
<p className="text-sm text-muted-foreground">Created</p>
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
{new Date(league.createdAt).toLocaleDateString()}
|
2025-10-11 00:07:39 -07:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-10-14 21:54:46 -07:00
|
|
|
{season && (
|
2025-10-15 21:50:02 -07:00
|
|
|
<>
|
2026-03-07 15:57:51 -08:00
|
|
|
{isDraftOrPreDraft && (
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Teams Filled
|
|
|
|
|
</p>
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
{teamsWithOwners} of {teams.length}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{isDraftOrPreDraft && (
|
2026-03-05 10:07:15 -08:00
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Draft Order Set
|
2025-10-21 21:36:42 -07:00
|
|
|
</p>
|
2026-03-05 10:07:15 -08:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
{isDraftOrderSet ? "Yes" : "No"}
|
|
|
|
|
</p>
|
|
|
|
|
{!isDraftOrderSet && isUserCommissioner && (
|
|
|
|
|
<Button size="sm" variant="outline" asChild>
|
|
|
|
|
<Link to={`/leagues/${league.id}/settings#draft-order`}>
|
|
|
|
|
Set Order
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-21 21:36:42 -07:00
|
|
|
</div>
|
2026-03-05 10:07:15 -08:00
|
|
|
)}
|
2025-10-15 21:50:02 -07:00
|
|
|
<div>
|
2025-10-20 15:53:38 -07:00
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Draft Rounds
|
|
|
|
|
</p>
|
2025-10-15 21:50:02 -07:00
|
|
|
<p className="font-medium">{season.draftRounds}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2025-10-20 15:53:38 -07:00
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Sports Selected
|
|
|
|
|
</p>
|
2025-10-15 21:50:02 -07:00
|
|
|
<p className="font-medium">{sportsCount}</p>
|
|
|
|
|
</div>
|
2025-10-20 15:53:38 -07:00
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">Draft Board</p>
|
|
|
|
|
<Link
|
|
|
|
|
to={`/leagues/${league.id}/draft-board/${season.id}`}
|
2026-02-20 19:26:11 -08:00
|
|
|
className="text-electric hover:underline font-medium"
|
2025-10-20 15:53:38 -07:00
|
|
|
>
|
|
|
|
|
View Draft Board
|
|
|
|
|
</Link>
|
2025-11-13 13:24:03 -08:00
|
|
|
</div>
|
2026-03-07 15:57:51 -08:00
|
|
|
{isActiveOrCompleted && (
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">Standings</p>
|
|
|
|
|
<Link
|
|
|
|
|
to={`/leagues/${league.id}/standings/${season.id}`}
|
|
|
|
|
className="text-electric hover:underline font-medium"
|
|
|
|
|
>
|
|
|
|
|
View Standings
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-15 21:50:02 -07:00
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">Flex Spots</p>
|
|
|
|
|
<p className="font-medium text-primary">
|
|
|
|
|
{Math.max(0, season.draftRounds - sportsCount)}
|
|
|
|
|
</p>
|
2026-03-07 15:57:51 -08:00
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Picks not tied to a specific sport
|
|
|
|
|
</p>
|
2025-10-15 21:50:02 -07:00
|
|
|
</div>
|
2025-10-15 22:02:21 -07:00
|
|
|
{season.draftDateTime && (
|
|
|
|
|
<div>
|
2025-10-20 15:53:38 -07:00
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Draft Date
|
|
|
|
|
</p>
|
2025-10-15 22:02:21 -07:00
|
|
|
<p className="font-medium">
|
|
|
|
|
{format(new Date(season.draftDateTime), "PPP 'at' p")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-15 21:50:02 -07:00
|
|
|
</>
|
2025-10-14 21:54:46 -07:00
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2025-10-11 00:29:04 -07:00
|
|
|
|
2025-10-14 21:54:46 -07:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Commissioners</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{commissioners.length} commissioner
|
|
|
|
|
{commissioners.length !== 1 ? "s" : ""}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{commissioners.map((commissioner) => {
|
|
|
|
|
const commissionerName = commissionerMap[commissioner.userId];
|
2026-02-20 08:45:09 -08:00
|
|
|
const hasTeam = teams.some(
|
|
|
|
|
(t) => t.ownerId === commissioner.userId
|
|
|
|
|
);
|
2025-10-14 21:54:46 -07:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={commissioner.id}
|
|
|
|
|
className="py-2 border-b last:border-0"
|
|
|
|
|
>
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
{commissionerName || "Unknown User"}
|
|
|
|
|
{commissioner.userId === currentUserId && (
|
2025-10-14 22:04:37 -07:00
|
|
|
<span className="ml-2 text-xs text-primary">
|
|
|
|
|
(You)
|
|
|
|
|
</span>
|
2025-10-14 21:54:46 -07:00
|
|
|
)}
|
|
|
|
|
</p>
|
2026-02-20 08:45:09 -08:00
|
|
|
{!hasTeam && (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
No team
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-10-14 21:54:46 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
2025-10-11 00:07:39 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|