diff --git a/app/routes/leagues/$leagueId.server.ts b/app/routes/leagues/$leagueId.server.ts index ec662c4..c34fd65 100644 --- a/app/routes/leagues/$leagueId.server.ts +++ b/app/routes/leagues/$leagueId.server.ts @@ -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, }; } diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index cdb64c0..0b2bf18 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -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 (
@@ -664,9 +679,42 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone

{league.name}

+ {season?.inviteCode && ( + + + Invite Link + + Share this link to invite people to join your league + {teamCount - teamsWithOwners > 0 && ` (${teamCount - teamsWithOwners} spot${teamCount - teamsWithOwners !== 1 ? "s" : ""} available)`} + + + +
+ e.currentTarget.select()} + /> + +
+

+ Anyone with this link can join your league +

+
+
+ )} +
- + {/* General Settings */} diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx index a90d8f7..883b77e 100644 --- a/app/routes/leagues/$leagueId.tsx +++ b/app/routes/leagues/$leagueId.tsx @@ -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) {

{league.name}

- {teams.find((t) => t.ownerId === currentUserId) && ( + {myTeam && ( @@ -121,6 +120,63 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
{/* Left Column - 2/3 width on desktop */}
+ {/* Standings Panel - active/completed seasons */} + {season && (season.status === "active" || season.status === "completed") && teams.length > 0 && ( + + +
+
+ Standings + Current season standings +
+ +
+
+ +
+ {[...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 ( +
+
+ {standing ? standing.currentRank : "T1"} +
+
+ + {team.name} + + {ownerName && ( +

+ {ownerName} +

+ )} +
+
+ {standing ? standing.totalPoints : 0} pts +
+
+ ); + })} +
+
+
+ )} + {/* Sports Seasons Section */} {season && sportsSeasons.length > 0 && ( @@ -148,7 +204,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { )} - {isUserCommissioner && season && availableTeamCount > 0 && ( + {isUserCommissioner && season && season.status === "pre_draft" && availableTeamCount > 0 && ( Invite Link @@ -182,7 +238,8 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { )} - + {/* Teams card - pre_draft/draft only; replaced by standings panel post-draft */} + {season && (season.status === "pre_draft" || season.status === "draft") && Teams @@ -225,64 +282,11 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { })}
- + }
{/* Right Column - 1/3 width on desktop */}
- {/* Draft Order Display - Only show during pre_draft and draft */} - {season && - (season.status === "pre_draft" || season.status === "draft") && - draftSlots.length > 0 && ( - - -
-
- Draft Order - - {season.status === "pre_draft" - ? "Upcoming draft order" - : "Current draft order"} - -
- -
-
- -
- {draftSlots.map((slot: any, index: number) => { - const ownerName = slot.team.ownerId - ? ownerMap[slot.team.ownerId] - : null; - return ( -
-
- {index + 1} -
-
-

- {slot.team.name} -

- {ownerName && ( -

- {ownerName} -

- )} -
-
- ); - })} -
-
-
- )} @@ -313,23 +317,25 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { {teamsWithOwners} of {teams.length}

-
-

- Draft Order Set -

-
-

- {isDraftOrderSet ? "Yes" : "No"} + {(season.status === "pre_draft" || season.status === "draft") && ( +

+

+ Draft Order Set

- {!isDraftOrderSet && isUserCommissioner && ( - - )} +
+

+ {isDraftOrderSet ? "Yes" : "No"} +

+ {!isDraftOrderSet && isUserCommissioner && ( + + )} +
-
+ )}

Draft Rounds