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 {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue