League page fixes (#64)
This commit is contained in:
parent
472ceca92d
commit
8c707d2e79
3 changed files with 139 additions and 79 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import { getAuth } from "@clerk/react-router/server";
|
import { getAuth } from "@clerk/react-router/server";
|
||||||
import {
|
import {
|
||||||
findLeagueById,
|
findLeagueById,
|
||||||
findCurrentSeason,
|
|
||||||
findTeamsBySeasonId,
|
findTeamsBySeasonId,
|
||||||
findCommissionersByLeagueId,
|
findCommissionersByLeagueId,
|
||||||
isUserLeagueMember,
|
isUserLeagueMember,
|
||||||
|
|
@ -10,6 +9,7 @@ import {
|
||||||
findDraftSlotsBySeasonId,
|
findDraftSlotsBySeasonId,
|
||||||
} from "~/models";
|
} from "~/models";
|
||||||
import { findCurrentSeasonWithSports } from "~/models/season";
|
import { findCurrentSeasonWithSports } from "~/models/season";
|
||||||
|
import { getSeasonStandings } from "~/models/standings";
|
||||||
import type { Route } from "./+types/$leagueId";
|
import type { Route } from "./+types/$leagueId";
|
||||||
|
|
||||||
export async function loader(args: Route.LoaderArgs) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
|
|
@ -64,6 +64,12 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
? await findDraftSlotsBySeasonId(season.id)
|
? 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
|
// Fetch user data for team owners
|
||||||
const ownerIds = teams
|
const ownerIds = teams
|
||||||
.map((t) => t.ownerId)
|
.map((t) => t.ownerId)
|
||||||
|
|
@ -128,10 +134,10 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
ownerMap: Object.fromEntries(ownerMap),
|
ownerMap: Object.fromEntries(ownerMap),
|
||||||
commissionerMap: Object.fromEntries(commissionerMap),
|
commissionerMap: Object.fromEntries(commissionerMap),
|
||||||
availableTeamCount,
|
availableTeamCount,
|
||||||
draftSlots,
|
|
||||||
sportsCount,
|
sportsCount,
|
||||||
teamsWithOwners,
|
teamsWithOwners,
|
||||||
isDraftOrderSet,
|
isDraftOrderSet,
|
||||||
sportsSeasons,
|
sportsSeasons,
|
||||||
|
standings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ import {
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "~/components/ui/alert-dialog";
|
} from "~/components/ui/alert-dialog";
|
||||||
import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor";
|
import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export async function loader(args: Route.LoaderArgs) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
const { userId } = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
|
|
@ -652,6 +653,20 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
return teams.find((t) => t.id === teamId);
|
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 (
|
return (
|
||||||
<div className="container max-w-2xl mx-auto py-8 px-4">
|
<div className="container max-w-2xl mx-auto py-8 px-4">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|
@ -664,9 +679,42 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
<p className="text-muted-foreground">{league.name}</p>
|
<p className="text-muted-foreground">{league.name}</p>
|
||||||
</div>
|
</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">
|
<Form method="post" className="space-y-6">
|
||||||
<input type="hidden" name="intent" value="update" />
|
<input type="hidden" name="intent" value="update" />
|
||||||
|
|
||||||
{/* General Settings */}
|
{/* General Settings */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,13 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
ownerMap,
|
ownerMap,
|
||||||
commissionerMap,
|
commissionerMap,
|
||||||
availableTeamCount,
|
availableTeamCount,
|
||||||
draftSlots,
|
|
||||||
sportsCount,
|
sportsCount,
|
||||||
teamsWithOwners,
|
teamsWithOwners,
|
||||||
isDraftOrderSet,
|
isDraftOrderSet,
|
||||||
sportsSeasons,
|
sportsSeasons,
|
||||||
|
standings,
|
||||||
} = loaderData;
|
} = loaderData;
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
@ -70,11 +71,9 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h1 className="text-4xl font-bold">{league.name}</h1>
|
<h1 className="text-4xl font-bold">{league.name}</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{teams.find((t) => t.ownerId === currentUserId) && (
|
{myTeam && (
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link
|
<Link to={`/teams/${myTeam.id}/settings`}>
|
||||||
to={`/teams/${teams.find((t) => t.ownerId === currentUserId)?.id}/settings`}
|
|
||||||
>
|
|
||||||
Team Settings
|
Team Settings
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -121,6 +120,63 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
{/* 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 */}
|
||||||
|
{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 */}
|
{/* Sports Seasons Section */}
|
||||||
{season && sportsSeasons.length > 0 && (
|
{season && sportsSeasons.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -148,7 +204,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isUserCommissioner && season && availableTeamCount > 0 && (
|
{isUserCommissioner && season && season.status === "pre_draft" && availableTeamCount > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Invite Link</CardTitle>
|
<CardTitle>Invite Link</CardTitle>
|
||||||
|
|
@ -182,7 +238,8 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card>
|
{/* Teams card - pre_draft/draft only; replaced by standings panel post-draft */}
|
||||||
|
{season && (season.status === "pre_draft" || season.status === "draft") && <Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Teams</CardTitle>
|
<CardTitle>Teams</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
|
@ -225,64 +282,11 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
||||||
{/* 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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -313,23 +317,25 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
{teamsWithOwners} of {teams.length}
|
{teamsWithOwners} of {teams.length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{(season.status === "pre_draft" || season.status === "draft") && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<div>
|
||||||
Draft Order Set
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
Draft Order Set
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="font-medium">
|
|
||||||
{isDraftOrderSet ? "Yes" : "No"}
|
|
||||||
</p>
|
</p>
|
||||||
{!isDraftOrderSet && isUserCommissioner && (
|
<div className="flex items-center gap-2">
|
||||||
<Button size="sm" variant="outline" asChild>
|
<p className="font-medium">
|
||||||
<Link to={`/leagues/${league.id}/settings#draft-order`}>
|
{isDraftOrderSet ? "Yes" : "No"}
|
||||||
Set Order
|
</p>
|
||||||
</Link>
|
{!isDraftOrderSet && isUserCommissioner && (
|
||||||
</Button>
|
<Button size="sm" variant="outline" asChild>
|
||||||
)}
|
<Link to={`/leagues/${league.id}/settings#draft-order`}>
|
||||||
|
Set Order
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Draft Rounds
|
Draft Rounds
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue