import { useEffect } from "react"; import { useSearchParams } from "react-router"; import { toast } from "sonner"; import { getAuth } from "@clerk/react-router/server"; import { addDays, subDays } from "date-fns"; import type { Route } from "./+types/home"; import { LandingPage } from "~/components/marketing/LandingPage"; import { findLeaguesWithActiveSeasonsByUserId } from "~/models/league"; import { findSeasonById, findCurrentSeasonWithSports } from "~/models/season"; import { findTeamByOwnerAndSeason } from "~/models/team"; import { getDraftedParticipantsBySportsSeason } from "~/models/draft-pick"; import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event"; import { getTeamStanding } from "~/models/standings"; import { getSeasonCompletionPercentage } from "~/lib/season-helpers.server"; import { findDraftSlotsBySeasonId } from "~/models/draft-slot"; import { getTeamForPick } from "~/lib/draft-order"; import type { CalendarPanelEvent } from "~/components/sport-season/UpcomingCalendarPanel"; import { UpcomingEventsCard } from "~/components/sport-season/UpcomingEventsCard"; import { MyLeaguesCard } from "~/components/league/MyLeaguesCard"; import { CreateLeagueCard } from "~/components/league/CreateLeagueCard"; import type { LeagueRowProps } from "~/components/league/LeagueRow"; import { toEventSortKey } from "~/lib/date-utils"; export function meta() { return [ { title: "Brackt - Fantasy All of the Sports!" }, { name: "description", content: "Play multi-sport fantasy leagues with your friends!", }, ]; } export async function loader(args: Route.LoaderArgs) { const { userId } = await getAuth(args); if (!userId) { return { leagues: [], isLoggedIn: false, upcomingCalendarEvents: [] }; } const leagues = await findLeaguesWithActiveSeasonsByUserId(userId); const today = new Date(); const dateFromStr = subDays(today, 1).toISOString().split("T")[0]; const dateToStr = addDays(today, 30).toISOString().split("T")[0]; const leaguesWithData = await Promise.all( leagues.map(async (league) => { const [season, seasonWithSports] = await Promise.all([ league.currentSeasonId ? findSeasonById(league.currentSeasonId) : null, findCurrentSeasonWithSports(league.id), ]); const numSports = seasonWithSports?.seasonSports?.length ?? 0; const calendarEvents: CalendarPanelEvent[] = []; let currentRank: number | undefined; let totalPoints: number | undefined; let previousRank: number | undefined; let completionPercentage = 0; let picksUntilMyTurn: number | undefined; let draftPosition: number | undefined; if (league.currentSeasonId && seasonWithSports?.seasonSports) { const myTeam = await findTeamByOwnerAndSeason(userId, league.currentSeasonId); if (myTeam) { // Fetch standing, completion %, and calendar events in parallel const [standing, pct, participantsBySportsSeason] = await Promise.all([ getTeamStanding(myTeam.id, league.currentSeasonId), getSeasonCompletionPercentage(league.currentSeasonId), getDraftedParticipantsBySportsSeason(myTeam.id, league.currentSeasonId), ]); completionPercentage = pct; // Draft slot info — needed for pre-draft position and in-progress picks-until-turn if (season?.status === "draft" || season?.status === "pre_draft") { const draftSlots = await findDraftSlotsBySeasonId(league.currentSeasonId); const mySlot = draftSlots.find((s) => s.teamId === myTeam.id); if (mySlot) { draftPosition = mySlot.draftOrder; } if (season.status === "draft" && season.currentPickNumber) { const currentPick = season.currentPickNumber; let offset = 0; // Snake draft: one full cycle is draftSlots.length * 2 picks while (offset < draftSlots.length * 2) { const slot = getTeamForPick(currentPick + offset, draftSlots); if (slot?.teamId === myTeam.id) { picksUntilMyTurn = offset; break; } offset++; } } } if (standing) { currentRank = standing.currentRank ?? undefined; totalPoints = standing.totalPoints ?? undefined; previousRank = standing.previousRank ?? undefined; } else if (myTeam && season?.status === "active") { // No standing row yet (no scoring events) — default to rank 1, 0 points currentRank = 1; totalPoints = 0; } const perSeasonEvents = await Promise.all( seasonWithSports.seasonSports.map(async ({ sportsSeason: ss }) => { const draftedParticipants = participantsBySportsSeason.get(ss.id) ?? []; if (draftedParticipants.length === 0) return []; const events = await getUpcomingEventsForDraftedParticipants( ss.id, ss.scoringPattern ?? "", draftedParticipants, dateFromStr, dateToStr ); return events.map((event) => ({ ...event, sportName: ss.sport.name, sportSeasonName: ss.name, leagueName: league.name, leagueId: league.id, sportsSeasonPageUrl: `/leagues/${league.id}/sports-seasons/${ss.id}`, })); }) ); calendarEvents.push(...perSeasonEvents.flat()); } } return { ...league, currentSeason: season, numSports, currentRank, totalPoints, previousRank, completionPercentage, picksUntilMyTurn, draftPosition, draftDateTime: season?.draftDateTime?.toISOString() ?? null, calendarEvents, }; }) ); const upcomingCalendarEvents = leaguesWithData .flatMap((l) => l.calendarEvents) .toSorted((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b))); return { leagues: leaguesWithData, isLoggedIn: true, upcomingCalendarEvents, }; } const VALID_STATUSES = ["draft", "active", "pre_draft", "completed"] as const; type ValidStatus = (typeof VALID_STATUSES)[number]; function isValidStatus(s: string): s is ValidStatus { return VALID_STATUSES.includes(s as ValidStatus); } export default function Home({ loaderData }: Route.ComponentProps) { const { leagues, isLoggedIn, upcomingCalendarEvents } = loaderData; const [searchParams, setSearchParams] = useSearchParams(); useEffect(() => { if (searchParams.get("deleted") === "true") { toast.success("League deleted successfully"); setSearchParams({}); } if (searchParams.get("left") === "true") { toast.success("You have left the league. Your team is now available."); setSearchParams({}); } }, [searchParams, setSearchParams]); if (!isLoggedIn) { return ; } const leagueRows: LeagueRowProps[] = leagues.map((league) => { const rawStatus = league.currentSeason?.status ?? "pre_draft"; return { leagueId: league.id, leagueName: league.name, numSports: league.numSports, status: isValidStatus(rawStatus) ? rawStatus : "pre_draft", seasonId: league.currentSeasonId ?? undefined, currentRank: league.currentRank, totalPoints: league.totalPoints, previousRank: league.previousRank, completionPercentage: league.completionPercentage, picksUntilMyTurn: league.picksUntilMyTurn, draftPosition: league.draftPosition, draftDateTime: league.draftDateTime, }; }); return (
{/* lg+: 3-col grid, left column (My Leagues + Create) spans 2, right (Events) spans 1. mobile/tablet: single column stacked in order: My Leagues, Upcoming Events, Create League. */}
{/* My Leagues — order 1 on mobile, col-span-2 on desktop */}
{/* Upcoming Events — order 2 on mobile, right column on desktop spanning both rows */}
1} />
{/* Create a League — order 3 on mobile, below My Leagues on desktop */}
); }