2025-10-11 00:29:04 -07:00
|
|
|
import { useEffect } from "react";
|
2025-10-11 21:10:01 -07:00
|
|
|
import { Link, useSearchParams } from "react-router";
|
2025-10-11 00:29:04 -07:00
|
|
|
import { toast } from "sonner";
|
2025-10-11 21:10:01 -07:00
|
|
|
import { getAuth } from "@clerk/react-router/server";
|
2026-03-12 10:12:38 -07:00
|
|
|
import { addDays } from "date-fns";
|
2025-10-10 23:04:50 -07:00
|
|
|
|
|
|
|
|
import type { Route } from "./+types/home";
|
2025-10-11 21:10:01 -07:00
|
|
|
import { findLeaguesWithActiveSeasonsByUserId } from "~/models/league";
|
2026-03-12 10:12:38 -07:00
|
|
|
import { findSeasonById, findCurrentSeasonWithSports } from "~/models/season";
|
|
|
|
|
import { findTeamByOwnerAndSeason } from "~/models/team";
|
|
|
|
|
import { getDraftedParticipantsBySportsSeason } from "~/models/draft-pick";
|
|
|
|
|
import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event";
|
|
|
|
|
import type { CalendarPanelEvent } from "~/components/sport-season/UpcomingCalendarPanel";
|
|
|
|
|
import { UpcomingCalendarPanel } from "~/components/sport-season/UpcomingCalendarPanel";
|
2026-03-15 10:22:42 -07:00
|
|
|
import { toEventSortKey } from "~/lib/date-utils";
|
2025-10-11 21:10:01 -07:00
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from "~/components/ui/card";
|
2025-10-10 23:04:50 -07:00
|
|
|
|
2025-10-11 01:03:58 -07:00
|
|
|
export function meta() {
|
2025-10-10 23:04:50 -07:00
|
|
|
return [
|
2025-10-13 10:04:32 -07:00
|
|
|
{ title: "Brackt - Fantasy All of the Sports!" },
|
2025-10-11 01:03:58 -07:00
|
|
|
{
|
|
|
|
|
name: "description",
|
2025-10-13 10:04:32 -07:00
|
|
|
content: "Play multi-sport fantasy leagues with your friends!",
|
2025-10-11 01:03:58 -07:00
|
|
|
},
|
2025-10-10 23:04:50 -07:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-11 21:10:01 -07:00
|
|
|
export async function loader(args: Route.LoaderArgs) {
|
|
|
|
|
const { userId } = await getAuth(args);
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
2026-03-12 10:12:38 -07:00
|
|
|
return { leagues: [], isLoggedIn: false, upcomingCalendarEvents: [] };
|
2025-10-11 21:10:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch leagues where user has a team in the current season
|
|
|
|
|
const leagues = await findLeaguesWithActiveSeasonsByUserId(userId);
|
|
|
|
|
|
2026-03-12 10:12:38 -07:00
|
|
|
const today = new Date();
|
|
|
|
|
const calendarDateFrom = today;
|
|
|
|
|
const calendarDateTo = addDays(today, 30);
|
|
|
|
|
|
|
|
|
|
// Fetch season details and calendar events in parallel per league
|
|
|
|
|
const leaguesWithData = await Promise.all(
|
2025-10-11 21:10:01 -07:00
|
|
|
leagues.map(async (league) => {
|
2026-03-12 10:12:38 -07:00
|
|
|
const [season, seasonWithSports] = await Promise.all([
|
|
|
|
|
league.currentSeasonId ? findSeasonById(league.currentSeasonId) : null,
|
|
|
|
|
findCurrentSeasonWithSports(league.id),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Build calendar events for this league
|
|
|
|
|
const calendarEvents: CalendarPanelEvent[] = [];
|
|
|
|
|
|
|
|
|
|
if (league.currentSeasonId && seasonWithSports?.seasonSports) {
|
|
|
|
|
const myTeam = await findTeamByOwnerAndSeason(userId, league.currentSeasonId);
|
|
|
|
|
if (myTeam) {
|
|
|
|
|
const participantsBySportsSeason = await getDraftedParticipantsBySportsSeason(
|
|
|
|
|
myTeam.id,
|
|
|
|
|
league.currentSeasonId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
calendarDateFrom,
|
|
|
|
|
calendarDateTo
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-11 21:10:01 -07:00
|
|
|
return {
|
|
|
|
|
...league,
|
|
|
|
|
currentSeason: season,
|
2026-03-12 10:12:38 -07:00
|
|
|
calendarEvents,
|
2025-10-11 21:10:01 -07:00
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-12 10:12:38 -07:00
|
|
|
const upcomingCalendarEvents = leaguesWithData
|
|
|
|
|
.flatMap((l) => l.calendarEvents)
|
2026-03-15 10:22:42 -07:00
|
|
|
.sort((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
2026-03-12 10:12:38 -07:00
|
|
|
|
2025-10-10 23:04:50 -07:00
|
|
|
return {
|
2026-03-12 10:12:38 -07:00
|
|
|
leagues: leaguesWithData,
|
2025-10-11 21:10:01 -07:00
|
|
|
isLoggedIn: true,
|
2026-03-12 10:12:38 -07:00
|
|
|
upcomingCalendarEvents,
|
2025-10-10 23:04:50 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-11 01:03:31 -07:00
|
|
|
export default function Home({ loaderData }: Route.ComponentProps) {
|
2026-03-12 10:12:38 -07:00
|
|
|
const { leagues, isLoggedIn, upcomingCalendarEvents } = loaderData;
|
2025-10-11 00:29:04 -07:00
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (searchParams.get("deleted") === "true") {
|
|
|
|
|
toast.success("League deleted successfully");
|
|
|
|
|
setSearchParams({});
|
|
|
|
|
}
|
2025-10-14 22:45:24 -07:00
|
|
|
if (searchParams.get("left") === "true") {
|
|
|
|
|
toast.success("You have left the league. Your team is now available.");
|
|
|
|
|
setSearchParams({});
|
|
|
|
|
}
|
2025-10-11 00:29:04 -07:00
|
|
|
}, [searchParams, setSearchParams]);
|
|
|
|
|
|
2025-10-11 21:10:01 -07:00
|
|
|
if (!isLoggedIn) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="container mx-auto py-16 px-4 text-center">
|
|
|
|
|
<h1 className="text-4xl font-bold mb-4">Welcome to Brackt</h1>
|
|
|
|
|
<p className="text-xl text-muted-foreground mb-8">
|
|
|
|
|
Please sign in to view your leagues
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="container mx-auto py-8 px-4">
|
|
|
|
|
<div className="flex items-center justify-between mb-8">
|
|
|
|
|
<h1 className="text-4xl font-bold">My Leagues</h1>
|
|
|
|
|
<Button asChild>
|
|
|
|
|
<Link to="/leagues/new">Create New League</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-12 10:12:38 -07:00
|
|
|
{upcomingCalendarEvents.length > 0 && (
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<UpcomingCalendarPanel events={upcomingCalendarEvents} showLeague={true} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-10-11 21:10:01 -07:00
|
|
|
{leagues.length === 0 ? (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
2026-02-20 10:28:37 -08:00
|
|
|
<CardTitle>No Leagues</CardTitle>
|
2025-10-11 21:10:01 -07:00
|
|
|
<CardDescription>
|
2026-02-20 10:28:37 -08:00
|
|
|
You aren't a member of any leagues yet
|
2025-10-11 21:10:01 -07:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Button asChild>
|
|
|
|
|
<Link to="/leagues/new">Create a New League</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{leagues.map((league) => (
|
|
|
|
|
<Link key={league.id} to={`/leagues/${league.id}`}>
|
|
|
|
|
<Card className="hover:border-primary transition-colors cursor-pointer h-full">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>{league.name}</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Created {new Date(league.createdAt).toLocaleDateString()}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{league.currentSeason ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Current Season
|
|
|
|
|
</p>
|
2025-10-13 10:04:32 -07:00
|
|
|
<p className="font-medium">
|
|
|
|
|
{league.currentSeason.year}
|
|
|
|
|
</p>
|
2025-10-11 21:10:01 -07:00
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">Status</p>
|
|
|
|
|
<p className="font-medium capitalize">
|
|
|
|
|
{league.currentSeason.status.replace("_", " ")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
No active season
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-10-10 23:04:50 -07:00
|
|
|
}
|