- Fix UTC midnight rollover bug: server now queries from yesterday UTC as a buffer; UpcomingCalendarPanel filters to local-today client-side via useEffect + Intl.DateTimeFormat, removing the need for any cookie or server-side timezone detection - Cap homepage and league page panels at 6 events with a "View all" link - Add /upcoming-events page (60-day view across all leagues) - Add /leagues/:leagueId/upcoming-events page (60-day per-league view) - Add emptyMessage prop to UpcomingCalendarPanel for context-specific copy - Change getUpcomingEventsForDraftedParticipants to accept pre-computed date strings instead of Date objects fixes #213 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
29ea93ad6d
commit
3c4ed67946
11 changed files with 317 additions and 26 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { format } from "date-fns";
|
||||
import { Calendar, Users } from "lucide-react";
|
||||
import { Calendar, ChevronRight, Users } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import {
|
||||
|
|
@ -24,6 +25,12 @@ interface Props {
|
|||
events: CalendarPanelEvent[];
|
||||
/** When true, renders a league name badge on each row */
|
||||
showLeague?: boolean;
|
||||
/** If provided, only the first N events are rendered */
|
||||
limit?: number;
|
||||
/** If provided and events.length > limit, renders a "View all" footer link */
|
||||
viewAllUrl?: string;
|
||||
/** Override the default empty state message */
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
function ParticipantList({ participants }: { participants: Array<{ id: string; name: string }> }) {
|
||||
|
|
@ -117,7 +124,19 @@ function EventRow({ event, showLeague }: { event: CalendarPanelEvent; showLeague
|
|||
return content;
|
||||
}
|
||||
|
||||
export function UpcomingCalendarPanel({ events, showLeague = false }: Props) {
|
||||
export function UpcomingCalendarPanel({ events, showLeague = false, limit, viewAllUrl, emptyMessage }: Props) {
|
||||
// Filter out past events client-side using local timezone. Server sends events
|
||||
// starting from yesterday UTC as a buffer; useEffect trims to today-and-forward
|
||||
// after hydration so the correct local date is used.
|
||||
const [localFilteredEvents, setLocalFilteredEvents] = useState(events);
|
||||
useEffect(() => {
|
||||
const todayStr = new Intl.DateTimeFormat("en-CA").format(new Date());
|
||||
setLocalFilteredEvents(events.filter((e) => (e.earliestGameTime ?? e.eventDate ?? "9999-12-31") >= todayStr));
|
||||
}, [events]);
|
||||
|
||||
const displayedEvents = limit !== undefined ? localFilteredEvents.slice(0, limit) : localFilteredEvents;
|
||||
const hiddenCount = limit !== undefined ? Math.max(0, localFilteredEvents.length - limit) : 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
|
|
@ -127,15 +146,28 @@ export function UpcomingCalendarPanel({ events, showLeague = false }: Props) {
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{events.length === 0 ? (
|
||||
{localFilteredEvents.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
No upcoming events in the next 30 days.
|
||||
{emptyMessage ?? "No upcoming events in the next 30 days."}
|
||||
</p>
|
||||
) : (
|
||||
<div>
|
||||
{events.map((event) => (
|
||||
{displayedEvents.map((event) => (
|
||||
<EventRow key={event.id} event={event} showLeague={showLeague} />
|
||||
))}
|
||||
{viewAllUrl && (
|
||||
<div className="mt-3 pt-2 border-t">
|
||||
<Link
|
||||
to={viewAllUrl}
|
||||
className="flex items-center gap-1 text-sm text-electric hover:underline"
|
||||
>
|
||||
{hiddenCount > 0
|
||||
? `View all ${localFilteredEvents.length} upcoming events`
|
||||
: "View all upcoming events"}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function makeEvent(overrides: Partial<CalendarPanelEvent> = {}): CalendarPanelEv
|
|||
id: overrides.id ?? "event-1",
|
||||
name: overrides.name ?? "UCL Quarterfinals",
|
||||
// Use explicit key check so callers can pass null to test the null-date path
|
||||
eventDate: "eventDate" in overrides ? (overrides.eventDate as string | null) : "2025-04-09",
|
||||
eventDate: "eventDate" in overrides ? (overrides.eventDate as string | null) : "2099-04-09",
|
||||
earliestGameTime: "earliestGameTime" in overrides ? (overrides.earliestGameTime as string | null) : null,
|
||||
matchLabel: overrides.matchLabel ?? null,
|
||||
eventType: overrides.eventType ?? "playoff_game",
|
||||
|
|
@ -45,7 +45,7 @@ describe("UpcomingCalendarPanel", () => {
|
|||
describe("event display", () => {
|
||||
it("shows event name and formatted date", () => {
|
||||
renderWithRouter(
|
||||
<UpcomingCalendarPanel events={[makeEvent({ name: "UCL QF", eventDate: "2025-04-09" })]} />
|
||||
<UpcomingCalendarPanel events={[makeEvent({ name: "UCL QF", eventDate: "2099-04-09" })]} />
|
||||
);
|
||||
expect(screen.getByText("UCL QF")).toBeInTheDocument();
|
||||
expect(screen.getByText("Apr 9")).toBeInTheDocument();
|
||||
|
|
@ -101,7 +101,7 @@ describe("UpcomingCalendarPanel", () => {
|
|||
it("uses earliestGameTime date when eventDate is null", () => {
|
||||
renderWithRouter(
|
||||
<UpcomingCalendarPanel
|
||||
events={[makeEvent({ eventDate: null, earliestGameTime: "2025-04-09T14:00:00.000Z" })]}
|
||||
events={[makeEvent({ eventDate: null, earliestGameTime: "2099-04-09T14:00:00.000Z" })]}
|
||||
/>
|
||||
);
|
||||
// Should not show TBD
|
||||
|
|
|
|||
|
|
@ -142,3 +142,4 @@ describe("toEventSortKey", () => {
|
|||
expect(dateOnly < withTime).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ vi.mock("drizzle-orm", () => ({
|
|||
import { getUpcomingEventsForDraftedParticipants } from "../scoring-event";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
const DATE_FROM = new Date("2025-04-01");
|
||||
const DATE_TO = new Date("2025-04-30");
|
||||
const DATE_FROM = "2025-04-01";
|
||||
const DATE_TO = "2025-04-30";
|
||||
const SPORTS_SEASON_ID = "ss-1";
|
||||
|
||||
function makeParticipant(id: string, name: string) {
|
||||
|
|
|
|||
|
|
@ -373,15 +373,13 @@ export async function getUpcomingEventsForDraftedParticipants(
|
|||
sportsSeasonId: string,
|
||||
scoringPattern: string,
|
||||
draftedParticipants: Array<{ id: string; name: string }>,
|
||||
dateFrom: Date,
|
||||
dateTo: Date,
|
||||
dateFromStr: string,
|
||||
dateToStr: string,
|
||||
providedDb?: ReturnType<typeof database>
|
||||
): Promise<UpcomingParticipantEvent[]> {
|
||||
if (draftedParticipants.length === 0) return [];
|
||||
|
||||
const db = providedDb || database();
|
||||
const dateFromStr = dateFrom.toISOString().split("T")[0];
|
||||
const dateToStr = dateTo.toISOString().split("T")[0];
|
||||
const draftedIds = draftedParticipants.map((p) => p.id);
|
||||
const draftedMap = new Map(draftedParticipants.map((p) => [p.id, p]));
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export default [
|
|||
route("leagues/new", "routes/leagues/new.tsx"),
|
||||
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
||||
route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"),
|
||||
route(
|
||||
"leagues/:leagueId/upcoming-events",
|
||||
"routes/leagues/$leagueId.upcoming-events.tsx"
|
||||
),
|
||||
route(
|
||||
"leagues/:leagueId/sports-seasons/:sportsSeasonId",
|
||||
"routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx"
|
||||
|
|
@ -47,6 +51,7 @@ export default [
|
|||
route("how-to-play", "routes/how-to-play.tsx"),
|
||||
route("rules", "routes/rules.tsx"),
|
||||
route("support", "routes/support.tsx"),
|
||||
route("upcoming-events", "routes/upcoming-events.tsx"),
|
||||
route("test-socket", "routes/test-socket.tsx"),
|
||||
|
||||
// Admin routes
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect } from "react";
|
|||
import { Link, useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { addDays } from "date-fns";
|
||||
import { addDays, subDays } from "date-fns";
|
||||
|
||||
import type { Route } from "./+types/home";
|
||||
import { findLeaguesWithActiveSeasonsByUserId } from "~/models/league";
|
||||
|
|
@ -43,8 +43,8 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
const leagues = await findLeaguesWithActiveSeasonsByUserId(userId);
|
||||
|
||||
const today = new Date();
|
||||
const calendarDateFrom = today;
|
||||
const calendarDateTo = addDays(today, 30);
|
||||
const dateFromStr = subDays(today, 1).toISOString().split("T")[0];
|
||||
const dateToStr = addDays(today, 30).toISOString().split("T")[0];
|
||||
|
||||
// Fetch season details and calendar events in parallel per league
|
||||
const leaguesWithData = await Promise.all(
|
||||
|
|
@ -74,8 +74,8 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
ss.id,
|
||||
ss.scoringPattern ?? "",
|
||||
draftedParticipants,
|
||||
calendarDateFrom,
|
||||
calendarDateTo
|
||||
dateFromStr,
|
||||
dateToStr
|
||||
);
|
||||
|
||||
return events.map((event) => ({
|
||||
|
|
@ -149,7 +149,12 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||
|
||||
{upcomingCalendarEvents.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<UpcomingCalendarPanel events={upcomingCalendarEvents} showLeague={true} />
|
||||
<UpcomingCalendarPanel
|
||||
events={upcomingCalendarEvents}
|
||||
showLeague={true}
|
||||
limit={6}
|
||||
viewAllUrl="/upcoming-events"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { addDays } from "date-fns";
|
||||
import { addDays, subDays } from "date-fns";
|
||||
import { toEventSortKey } from "~/lib/date-utils";
|
||||
import {
|
||||
findLeagueById,
|
||||
|
|
@ -107,8 +107,8 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
const myTeam = teams.find((t) => t.ownerId === userId) ?? null;
|
||||
const today = new Date();
|
||||
const calendarDateFrom = today;
|
||||
const calendarDateTo = addDays(today, 30);
|
||||
const dateFromStr = subDays(today, 1).toISOString().split("T")[0];
|
||||
const dateToStr = addDays(today, 30).toISOString().split("T")[0];
|
||||
|
||||
const participantsBySportsSeason = myTeam && season
|
||||
? await getDraftedParticipantsBySportsSeason(myTeam.id, season.id)
|
||||
|
|
@ -123,8 +123,8 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
ss.id,
|
||||
ss.scoringPattern ?? "",
|
||||
draftedParticipants,
|
||||
calendarDateFrom,
|
||||
calendarDateTo
|
||||
dateFromStr,
|
||||
dateToStr
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -379,7 +379,12 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
{/* Right Column - 1/3 width on desktop */}
|
||||
<div className="space-y-6">
|
||||
{upcomingCalendarEvents.length > 0 && (
|
||||
<UpcomingCalendarPanel events={upcomingCalendarEvents} showLeague={false} />
|
||||
<UpcomingCalendarPanel
|
||||
events={upcomingCalendarEvents}
|
||||
showLeague={false}
|
||||
limit={6}
|
||||
viewAllUrl={`/leagues/${league.id}/upcoming-events`}
|
||||
/>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
|
|||
125
app/routes/leagues/$leagueId.upcoming-events.tsx
Normal file
125
app/routes/leagues/$leagueId.upcoming-events.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { addDays, subDays } from "date-fns";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import type { Route } from "./+types/$leagueId.upcoming-events";
|
||||
import {
|
||||
findLeagueById,
|
||||
isCommissioner,
|
||||
isUserLeagueMember,
|
||||
} from "~/models";
|
||||
import { 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";
|
||||
import { toEventSortKey } from "~/lib/date-utils";
|
||||
|
||||
export function meta({ data }: Route.MetaArgs) {
|
||||
const leagueName = data?.leagueName ?? "League";
|
||||
return [{ title: `${leagueName} — Upcoming Events — Brackt` }];
|
||||
}
|
||||
|
||||
export async function loader(args: Route.LoaderArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
const { leagueId } = args.params;
|
||||
|
||||
const league = await findLeagueById(leagueId);
|
||||
if (!league) {
|
||||
throw new Response("League not found", { status: 404 });
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new Response("You must be logged in to view this league", { status: 401 });
|
||||
}
|
||||
|
||||
const [isUserCommissioner, isUserMember] = await Promise.all([
|
||||
isCommissioner(leagueId, userId),
|
||||
isUserLeagueMember(leagueId, userId),
|
||||
]);
|
||||
|
||||
if (!isUserCommissioner && !isUserMember) {
|
||||
throw new Response("You do not have access to this league", { status: 403 });
|
||||
}
|
||||
|
||||
const seasonWithSports = await findCurrentSeasonWithSports(leagueId);
|
||||
|
||||
const today = new Date();
|
||||
const dateFromStr = subDays(today, 1).toISOString().split("T")[0];
|
||||
const dateToStr = addDays(today, 60).toISOString().split("T")[0];
|
||||
|
||||
const calendarEvents: CalendarPanelEvent[] = [];
|
||||
|
||||
if (seasonWithSports?.seasonSports && seasonWithSports.id) {
|
||||
const myTeam = await findTeamByOwnerAndSeason(userId, seasonWithSports.id);
|
||||
|
||||
if (myTeam) {
|
||||
const participantsBySportsSeason = await getDraftedParticipantsBySportsSeason(
|
||||
myTeam.id,
|
||||
seasonWithSports.id
|
||||
);
|
||||
|
||||
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,
|
||||
sportsSeasonPageUrl: `/leagues/${leagueId}/sports-seasons/${ss.id}`,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
calendarEvents.push(...perSeasonEvents.flat());
|
||||
}
|
||||
}
|
||||
|
||||
const upcomingCalendarEvents = calendarEvents.toSorted((a, b) =>
|
||||
toEventSortKey(a).localeCompare(toEventSortKey(b))
|
||||
);
|
||||
|
||||
return {
|
||||
leagueName: league.name,
|
||||
leagueId,
|
||||
upcomingCalendarEvents,
|
||||
};
|
||||
}
|
||||
|
||||
export default function LeagueUpcomingEventsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { leagueName, leagueId, upcomingCalendarEvents } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to={`/leagues/${leagueId}`}
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
{leagueName}
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold">Upcoming Events</h1>
|
||||
<p className="text-muted-foreground mt-1">Next 60 days · {leagueName}</p>
|
||||
</div>
|
||||
|
||||
<UpcomingCalendarPanel
|
||||
events={upcomingCalendarEvents}
|
||||
showLeague={false}
|
||||
emptyMessage="No upcoming events in the next 60 days for this league."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
app/routes/upcoming-events.tsx
Normal file
120
app/routes/upcoming-events.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { addDays, subDays } from "date-fns";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import type { Route } from "./+types/upcoming-events";
|
||||
import { findLeaguesWithActiveSeasonsByUserId } from "~/models/league";
|
||||
import { 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";
|
||||
import { toEventSortKey } from "~/lib/date-utils";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "Upcoming Events — Brackt" }];
|
||||
}
|
||||
|
||||
export async function loader(args: Route.LoaderArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
|
||||
if (!userId) {
|
||||
return { isLoggedIn: false as const, upcomingCalendarEvents: [] };
|
||||
}
|
||||
|
||||
const leagues = await findLeaguesWithActiveSeasonsByUserId(userId);
|
||||
|
||||
const today = new Date();
|
||||
const dateFromStr = subDays(today, 1).toISOString().split("T")[0];
|
||||
const dateToStr = addDays(today, 60).toISOString().split("T")[0];
|
||||
|
||||
const leaguesWithData = await Promise.all(
|
||||
leagues.map(async (league) => {
|
||||
const calendarEvents: CalendarPanelEvent[] = [];
|
||||
|
||||
if (league.currentSeasonId) {
|
||||
const [seasonWithSports, myTeam] = await Promise.all([
|
||||
findCurrentSeasonWithSports(league.id),
|
||||
findTeamByOwnerAndSeason(userId, league.currentSeasonId),
|
||||
]);
|
||||
|
||||
if (myTeam && seasonWithSports?.seasonSports) {
|
||||
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,
|
||||
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 calendarEvents;
|
||||
})
|
||||
);
|
||||
|
||||
const upcomingCalendarEvents = leaguesWithData
|
||||
.flat()
|
||||
.toSorted((a, b) => toEventSortKey(a).localeCompare(toEventSortKey(b)));
|
||||
|
||||
return { isLoggedIn: true as const, upcomingCalendarEvents };
|
||||
}
|
||||
|
||||
export default function UpcomingEventsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { isLoggedIn, upcomingCalendarEvents } = loaderData;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<div className="container mx-auto py-16 px-4 text-center">
|
||||
<p className="text-muted-foreground">Sign in to see your upcoming events.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
My Leagues
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold">Upcoming Events</h1>
|
||||
<p className="text-muted-foreground mt-1">Next 60 days across all your leagues</p>
|
||||
</div>
|
||||
|
||||
<UpcomingCalendarPanel
|
||||
events={upcomingCalendarEvents}
|
||||
showLeague={true}
|
||||
emptyMessage="No upcoming events in the next 60 days."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue