import { useLoaderData, Link } from "react-router"; import { getAuth } from "@clerk/react-router/server"; import { database } from "~/database/context"; import { eq, and } from "drizzle-orm"; import * as schema from "~/database/schema"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { StandingsTable } from "~/components/standings/StandingsTable"; import { logger } from "~/lib/logger"; import { getSevenDayStandingsChange, getSeasonPointProgression } from "~/models/standings"; import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-helpers.server"; import { PointProgressionChart } from "~/components/standings/PointProgressionChart"; import { findUsersByClerkIds, getUserDisplayName } from "~/models/user"; import type { Route } from "./+types/$leagueId.standings.$seasonId"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Standings — ${data?.league?.name ?? "League"} - Brackt` }]; } export async function loader(args: Route.LoaderArgs) { try { const { userId } = await getAuth(args); const { params } = args; const { leagueId, seasonId } = params; const db = database(); // Fetch league const league = await db.query.leagues.findFirst({ where: eq(schema.leagues.id, leagueId), }); if (!league) { throw new Response("League not found", { status: 404 }); } // Fetch season const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), }); if (!season || season.leagueId !== leagueId) { throw new Response("Season not found", { status: 404 }); } // Check access if (!userId) { throw new Response("You must be logged in to view standings", { status: 401, }); } // Check if user is a commissioner const isUserCommissioner = await db.query.commissioners.findFirst({ where: and( eq(schema.commissioners.leagueId, leagueId), eq(schema.commissioners.userId, userId) ), }); // Check if user has a team in this season const hasTeam = await db.query.teams.findFirst({ where: and( eq(schema.teams.seasonId, seasonId), eq(schema.teams.ownerId, userId) ), }); if (!isUserCommissioner && !hasTeam) { throw new Response("You do not have access to this league", { status: 403, }); } // Fetch standings, teams (for owner lookup), and independent data in parallel const [standingsWithComparison, teams, progressionData, seasonComplete, completionPercentage] = await Promise.all([ getSevenDayStandingsChange(seasonId, db), db.query.teams.findMany({ where: eq(schema.teams.seasonId, seasonId), columns: { id: true, ownerId: true }, }), getSeasonPointProgression(seasonId, db), isSeasonComplete(seasonId, db), getSeasonCompletionPercentage(seasonId, db), ]); // Build teamId -> ownerName map const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter((id): id is string => id !== null))]; const userRows = await findUsersByClerkIds(ownerIds); const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u])); const ownerNameByTeamId = new Map( teams .filter((t): t is typeof t & { ownerId: string } => t.ownerId !== null) .map((t) => { const user = userByClerkId.get(t.ownerId); return [t.id, user ? getUserDisplayName(user) : null] as const; }) ); // Map to format expected by component (use sevenDayRankChange instead of previousRank) const formattedStandings = standingsWithComparison.map((standing) => ({ ...standing, rankChange: standing.sevenDayRankChange, ownerName: ownerNameByTeamId.get(standing.teamId) ?? null, })); return { league, season, standings: formattedStandings, progressionData, seasonComplete, completionPercentage, }; } catch (error) { logger.error("Error loading standings:", error); logger.error("Error details:", { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); throw error; } } export default function LeagueStandings() { const { league, season, standings, progressionData, seasonComplete, completionPercentage } = useLoaderData(); return (
{/* Header */}

{league.name}

{season.year} Season Standings

{seasonComplete ? (
✓ Season Complete
) : (
{completionPercentage}% Complete
)}
{/* Point Progression Chart */} {progressionData.chartData.length > 0 && (
)} {/* Standings Card */} {seasonComplete ? "Final Standings" : "Current Standings"} {standings.length === 0 ? (

No standings data yet.

Standings will appear once participants have results.

) : ( )}
{/* Info Card */}

Projected Points: Shows actual points from finished participants plus expected value (EV) from remaining participants based on their probability distributions. The "+X.X EV" indicates how many additional points are expected from unfinished participants.

Tiebreaker Rules: Teams are ranked by total points. If tied, the team with more 1st place finishes ranks higher. If still tied, 2nd place finishes are compared, then 3rd, and so on through 8th place.

Movement Indicators: Arrows show rank changes compared to 7 days ago. Green arrows (↑) indicate improvement in rank, red arrows (↓) indicate decline.

Placement Breakdown: Shows how many times each team's participants finished in each position (1st×2 means 2 first-place finishes).

{progressionData.chartData.length > 0 && (

Point Progression Chart: Visualizes how each team's total points evolved over time based on {progressionData.chartData.length} day{progressionData.chartData.length !== 1 ? 's' : ''} of snapshot data. {seasonComplete && " Use this to see how the final standings developed throughout the season."}

)}
); }