League page fixes (#64)

This commit is contained in:
Chris Parsons 2026-03-05 10:07:15 -08:00 committed by GitHub
parent 472ceca92d
commit 8c707d2e79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 139 additions and 79 deletions

View file

@ -1,7 +1,6 @@
import { getAuth } from "@clerk/react-router/server";
import {
findLeagueById,
findCurrentSeason,
findTeamsBySeasonId,
findCommissionersByLeagueId,
isUserLeagueMember,
@ -10,6 +9,7 @@ import {
findDraftSlotsBySeasonId,
} from "~/models";
import { findCurrentSeasonWithSports } from "~/models/season";
import { getSeasonStandings } from "~/models/standings";
import type { Route } from "./+types/$leagueId";
export async function loader(args: Route.LoaderArgs) {
@ -64,6 +64,12 @@ export async function loader(args: Route.LoaderArgs) {
? await findDraftSlotsBySeasonId(season.id)
: [];
// Fetch standings for active/completed seasons
const standings =
season && (season.status === "active" || season.status === "completed")
? await getSeasonStandings(season.id)
: [];
// Fetch user data for team owners
const ownerIds = teams
.map((t) => t.ownerId)
@ -128,10 +134,10 @@ export async function loader(args: Route.LoaderArgs) {
ownerMap: Object.fromEntries(ownerMap),
commissionerMap: Object.fromEntries(commissionerMap),
availableTeamCount,
draftSlots,
sportsCount,
teamsWithOwners,
isDraftOrderSet,
sportsSeasons,
standings,
};
}

View file

@ -61,6 +61,7 @@ import {
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor";
import { toast } from "sonner";
export async function loader(args: Route.LoaderArgs) {
const { userId } = await getAuth(args);
@ -652,6 +653,20 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
return teams.find((t) => t.id === teamId);
};
const [copied, setCopied] = useState(false);
const handleCopyInviteLink = async () => {
if (!season?.inviteCode) return;
const inviteUrl = `${window.location.origin}/i/${season.inviteCode}`;
try {
await navigator.clipboard.writeText(inviteUrl);
setCopied(true);
toast.success("Invite link copied to clipboard!");
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error("Failed to copy invite link");
}
};
return (
<div className="container max-w-2xl mx-auto py-8 px-4">
<div className="mb-8">
@ -664,9 +679,42 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
<p className="text-muted-foreground">{league.name}</p>
</div>
{season?.inviteCode && (
<Card className="mb-6">
<CardHeader>
<CardTitle>Invite Link</CardTitle>
<CardDescription>
Share this link to invite people to join your league
{teamCount - teamsWithOwners > 0 && ` (${teamCount - teamsWithOwners} spot${teamCount - teamsWithOwners !== 1 ? "s" : ""} available)`}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<input
type="text"
readOnly
value={`${typeof window !== "undefined" ? window.location.origin : ""}/i/${season.inviteCode}`}
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>
)}
<Form method="post" className="space-y-6">
<input type="hidden" name="intent" value="update" />
{/* General Settings */}
<Card>
<CardHeader>

View file

@ -26,12 +26,13 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
ownerMap,
commissionerMap,
availableTeamCount,
draftSlots,
sportsCount,
teamsWithOwners,
isDraftOrderSet,
sportsSeasons,
standings,
} = loaderData;
const myTeam = teams.find((t) => t.ownerId === currentUserId);
const [searchParams, setSearchParams] = useSearchParams();
const [copied, setCopied] = useState(false);
@ -70,11 +71,9 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
<div className="flex items-center justify-between mb-2">
<h1 className="text-4xl font-bold">{league.name}</h1>
<div className="flex gap-2">
{teams.find((t) => t.ownerId === currentUserId) && (
{myTeam && (
<Button variant="outline" asChild>
<Link
to={`/teams/${teams.find((t) => t.ownerId === currentUserId)?.id}/settings`}
>
<Link to={`/teams/${myTeam.id}/settings`}>
Team Settings
</Link>
</Button>
@ -121,6 +120,63 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
<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">
{/* Standings Panel - active/completed seasons */}
{season && (season.status === "active" || season.status === "completed") && teams.length > 0 && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Standings</CardTitle>
<CardDescription>Current season standings</CardDescription>
</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">
{[...teams].sort((a, b) => {
const rankA = standings.find((s) => s.teamId === a.id)?.currentRank ?? Infinity;
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;
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">
{standing ? standing.currentRank : "T1"}
</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>
)}
{/* Sports Seasons Section */}
{season && sportsSeasons.length > 0 && (
<Card>
@ -148,7 +204,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
</Card>
)}
{isUserCommissioner && season && availableTeamCount > 0 && (
{isUserCommissioner && season && season.status === "pre_draft" && availableTeamCount > 0 && (
<Card>
<CardHeader>
<CardTitle>Invite Link</CardTitle>
@ -182,7 +238,8 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
</Card>
)}
<Card>
{/* Teams card - pre_draft/draft only; replaced by standings panel post-draft */}
{season && (season.status === "pre_draft" || season.status === "draft") && <Card>
<CardHeader>
<CardTitle>Teams</CardTitle>
<CardDescription>
@ -225,64 +282,11 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
})}
</div>
</CardContent>
</Card>
</Card>}
</div>
{/* Right Column - 1/3 width on desktop */}
<div className="space-y-6">
{/* Draft Order Display - Only show during pre_draft and draft */}
{season &&
(season.status === "pre_draft" || season.status === "draft") &&
draftSlots.length > 0 && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Draft Order</CardTitle>
<CardDescription>
{season.status === "pre_draft"
? "Upcoming draft order"
: "Current draft order"}
</CardDescription>
</div>
<Button asChild size="sm">
<Link to={`/leagues/${league.id}/draft/${season.id}`}>
Enter Draft Room
</Link>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{draftSlots.map((slot: any, index: number) => {
const ownerName = slot.team.ownerId
? ownerMap[slot.team.ownerId]
: null;
return (
<div
key={slot.id}
className="flex items-center gap-3 py-2 border-b last:border-0"
>
<div className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-primary-foreground font-bold text-xs">
{index + 1}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{slot.team.name}
</p>
{ownerName && (
<p className="text-xs text-muted-foreground truncate">
{ownerName}
</p>
)}
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
@ -313,23 +317,25 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
{teamsWithOwners} of {teams.length}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">
Draft Order Set
</p>
<div className="flex items-center gap-2">
<p className="font-medium">
{isDraftOrderSet ? "Yes" : "No"}
{(season.status === "pre_draft" || season.status === "draft") && (
<div>
<p className="text-sm text-muted-foreground">
Draft Order Set
</p>
{!isDraftOrderSet && isUserCommissioner && (
<Button size="sm" variant="outline" asChild>
<Link to={`/leagues/${league.id}/settings#draft-order`}>
Set Order
</Link>
</Button>
)}
<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>
</div>
</div>
)}
<div>
<p className="text-sm text-muted-foreground">
Draft Rounds