Apply code review fixes to CS2 event view and schedule
- Fix "Advanced" badge in SwissStageTab: condition was `isAdvanced && stageEliminated === null`, missing teams eliminated at later stages; now just `isAdvanced` - Default active tab to highest active Swiss stage rather than Champions whenever no playoff games have a winner yet - Remove dead `eventResults` query from the CS2 loader branch (Cs2MajorEventView has no prop for it) - Remove dead `teamsInStage` variable and unused `StageStatusBadge` component - Serialize `createdAt`/`updatedAt` on playoff match rows in both CS2 and bracket loader branches (matches how seasonMatches dates are handled) - Replace inline `formatEventDate` in event detail route with shared `~/lib/date-utils` version - Add defensive DB-level limit to `getUpcomingScoringEvents` (max(limit*5, 50)) to cap unbounded fetch https://claude.ai/code/session_01RYK7NCjcaah545979wupcM
This commit is contained in:
parent
df70ef83c2
commit
398a03e8e1
4 changed files with 31 additions and 58 deletions
|
|
@ -54,35 +54,6 @@ const STAGE_LABELS: Record<StageTab, string> = {
|
||||||
champions: "Champions",
|
champions: "Champions",
|
||||||
};
|
};
|
||||||
|
|
||||||
function StageStatusBadge({ result }: { result: Cs2StageResultWithName }) {
|
|
||||||
if (result.stageEliminated !== null) {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 text-xs text-red-400">
|
|
||||||
<XCircle className="h-3 w-3" />
|
|
||||||
Eliminated
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const winsNeeded = 3;
|
|
||||||
const wins = result.stageEliminatedWins ?? 0;
|
|
||||||
if (result.stageEliminated === null && result.stageEntry !== null) {
|
|
||||||
// Null stageEliminated = either in progress or advanced
|
|
||||||
// If their stageEntry is less than the current stage context, they advanced
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 text-xs text-emerald-400">
|
|
||||||
<CheckCircle2 className="h-3 w-3" />
|
|
||||||
Advanced
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
In Progress
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SwissStageTab({
|
function SwissStageTab({
|
||||||
stage,
|
stage,
|
||||||
stageResults,
|
stageResults,
|
||||||
|
|
@ -99,7 +70,6 @@ function SwissStageTab({
|
||||||
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
|
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
|
||||||
const userIds = new Set(userParticipantIds);
|
const userIds = new Set(userParticipantIds);
|
||||||
|
|
||||||
const teamsInStage = stageResults.filter((r) => r.stageEntry <= stage && (r.stageEliminated === null || r.stageEliminated >= stage));
|
|
||||||
const matchesInStage = matches.filter((m) => m.matchStage === stage);
|
const matchesInStage = matches.filter((m) => m.matchStage === stage);
|
||||||
|
|
||||||
// Group teams by W-L record (approximated from stageEliminatedWins if eliminated, else unknown)
|
// Group teams by W-L record (approximated from stageEliminatedWins if eliminated, else unknown)
|
||||||
|
|
@ -154,7 +124,7 @@ function SwissStageTab({
|
||||||
<span className="flex items-center gap-1 text-xs text-red-400">
|
<span className="flex items-center gap-1 text-xs text-red-400">
|
||||||
<XCircle className="h-3 w-3" /> Eliminated
|
<XCircle className="h-3 w-3" /> Eliminated
|
||||||
</span>
|
</span>
|
||||||
) : isAdvanced && r.stageEliminated === null ? (
|
) : isAdvanced ? (
|
||||||
<span className="flex items-center gap-1 text-xs text-emerald-400">
|
<span className="flex items-center gap-1 text-xs text-emerald-400">
|
||||||
<CheckCircle2 className="h-3 w-3" /> Advanced
|
<CheckCircle2 className="h-3 w-3" /> Advanced
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -218,8 +188,13 @@ export function Cs2MajorEventView({
|
||||||
teamOwnerships,
|
teamOwnerships,
|
||||||
userParticipantIds,
|
userParticipantIds,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const hasChampions = playoffMatches.length > 0;
|
const hasChampionsActivity = playoffMatches.some((m) => m.winnerId !== null);
|
||||||
const [activeTab, setActiveTab] = useState<StageTab>(hasChampions ? "champions" : 3);
|
const highestActiveStage: 1 | 2 | 3 =
|
||||||
|
cs2StageResults.some((r) => r.stageEntry >= 3 || r.stageEliminated === 3) ? 3 :
|
||||||
|
cs2StageResults.some((r) => r.stageEntry >= 2 || r.stageEliminated === 2) ? 2 : 1;
|
||||||
|
const [activeTab, setActiveTab] = useState<StageTab>(
|
||||||
|
hasChampionsActivity ? "champions" : highestActiveStage
|
||||||
|
);
|
||||||
|
|
||||||
const tabs: StageTab[] = [1, 2, 3, "champions"];
|
const tabs: StageTab[] = [1, 2, 3, "champions"];
|
||||||
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
|
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
|
||||||
|
|
@ -262,7 +237,7 @@ export function Cs2MajorEventView({
|
||||||
|
|
||||||
{/* Champions Stage bracket */}
|
{/* Champions Stage bracket */}
|
||||||
{activeTab === "champions" && (
|
{activeTab === "champions" && (
|
||||||
hasChampions ? (
|
playoffMatches.length > 0 ? (
|
||||||
<PlayoffBracket
|
<PlayoffBracket
|
||||||
matches={playoffMatches}
|
matches={playoffMatches}
|
||||||
rounds={playoffRounds}
|
rounds={playoffRounds}
|
||||||
|
|
|
||||||
|
|
@ -296,13 +296,15 @@ export async function getUpcomingScoringEvents(
|
||||||
) {
|
) {
|
||||||
const db = providedDb || database();
|
const db = providedDb || database();
|
||||||
|
|
||||||
// Fetch all incomplete events for the season (typically few per season).
|
// Over-fetch relative to the requested limit so the subsequent game-time
|
||||||
|
// re-sort has enough candidates without an unbounded query.
|
||||||
const events = await db.query.scoringEvents.findMany({
|
const events = await db.query.scoringEvents.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
||||||
eq(schema.scoringEvents.isComplete, false)
|
eq(schema.scoringEvents.isComplete, false)
|
||||||
),
|
),
|
||||||
orderBy: [asc(schema.scoringEvents.eventDate), asc(schema.scoringEvents.eventStartsAt), asc(schema.scoringEvents.createdAt)],
|
orderBy: [asc(schema.scoringEvents.eventDate), asc(schema.scoringEvents.eventStartsAt), asc(schema.scoringEvents.createdAt)],
|
||||||
|
limit: Math.max(limit * 5, 50),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (events.length === 0) return [];
|
if (events.length === 0) return [];
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||||
|
|
||||||
const simulatorType = sportsSeason.sport?.simulatorType ?? null;
|
const simulatorType = sportsSeason.sport?.simulatorType ?? null;
|
||||||
|
|
||||||
type PlayoffMatchWithRelations = PlayoffMatch & {
|
type PlayoffMatchWithRelations = Omit<PlayoffMatch, "createdAt" | "updatedAt"> & {
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
participant1: { id: string; name: string } | null;
|
participant1: { id: string; name: string } | null;
|
||||||
participant2: { id: string; name: string } | null;
|
participant2: { id: string; name: string } | null;
|
||||||
winner: { id: string; name: string } | null;
|
winner: { id: string; name: string } | null;
|
||||||
|
|
@ -115,17 +117,20 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||||
|
|
||||||
// CS2 Major — load Swiss stage data + bracket
|
// CS2 Major — load Swiss stage data + bracket
|
||||||
if (simulatorType === "cs2_major_qualifying_points") {
|
if (simulatorType === "cs2_major_qualifying_points") {
|
||||||
const [cs2StageResults, seasonMatches, playoffMatchRows, eventResultRows] = await Promise.all([
|
const [cs2StageResults, seasonMatches, playoffMatchRows] = await Promise.all([
|
||||||
getCs2StageResultsForEvent(eventId),
|
getCs2StageResultsForEvent(eventId),
|
||||||
findSeasonMatchesByScoringEventId(eventId),
|
findSeasonMatchesByScoringEventId(eventId),
|
||||||
db.query.playoffMatches.findMany({
|
db.query.playoffMatches.findMany({
|
||||||
where: eq(schema.playoffMatches.scoringEventId, eventId),
|
where: eq(schema.playoffMatches.scoringEventId, eventId),
|
||||||
with: { participant1: true, participant2: true, winner: true, loser: true },
|
with: { participant1: true, participant2: true, winner: true, loser: true },
|
||||||
}),
|
}),
|
||||||
getEventResults(eventId),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const playoffMatches = playoffMatchRows as PlayoffMatchWithRelations[];
|
const playoffMatches: PlayoffMatchWithRelations[] = playoffMatchRows.map((m) => ({
|
||||||
|
...m,
|
||||||
|
createdAt: m.createdAt.toISOString(),
|
||||||
|
updatedAt: m.updatedAt.toISOString(),
|
||||||
|
}));
|
||||||
const templateId = scoringEvent.bracketTemplateId ?? undefined;
|
const templateId = scoringEvent.bracketTemplateId ?? undefined;
|
||||||
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
||||||
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);
|
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);
|
||||||
|
|
@ -147,13 +152,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||||
playoffMatches,
|
playoffMatches,
|
||||||
playoffRounds,
|
playoffRounds,
|
||||||
bracketTemplateId: templateId ?? null,
|
bracketTemplateId: templateId ?? null,
|
||||||
eventResults: eventResultRows.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
placement: r.placement,
|
|
||||||
qualifyingPointsAwarded: r.qualifyingPointsAwarded,
|
|
||||||
seasonParticipantId: r.seasonParticipantId,
|
|
||||||
participantName: r.seasonParticipant?.name ?? null,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +161,11 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||||
where: eq(schema.playoffMatches.scoringEventId, eventId),
|
where: eq(schema.playoffMatches.scoringEventId, eventId),
|
||||||
with: { participant1: true, participant2: true, winner: true, loser: true },
|
with: { participant1: true, participant2: true, winner: true, loser: true },
|
||||||
});
|
});
|
||||||
const playoffMatches = playoffMatchRows as PlayoffMatchWithRelations[];
|
const playoffMatches: PlayoffMatchWithRelations[] = playoffMatchRows.map((m) => ({
|
||||||
|
...m,
|
||||||
|
createdAt: m.createdAt.toISOString(),
|
||||||
|
updatedAt: m.updatedAt.toISOString(),
|
||||||
|
}));
|
||||||
const templateId = scoringEvent.bracketTemplateId ?? undefined;
|
const templateId = scoringEvent.bracketTemplateId ?? undefined;
|
||||||
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
||||||
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);
|
const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { ArrowLeft, Calendar } from "lucide-react";
|
import { ArrowLeft, Calendar } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
|
||||||
import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId";
|
import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId";
|
||||||
import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server";
|
import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server";
|
||||||
|
import { formatEventDate } from "~/lib/date-utils";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -30,13 +30,6 @@ export function meta({ data }: Route.MetaArgs) {
|
||||||
|
|
||||||
export { loader };
|
export { loader };
|
||||||
|
|
||||||
function formatEventDate(dateStr: string | null | Date | undefined): string {
|
|
||||||
if (!dateStr) return "Date TBD";
|
|
||||||
const d = new Date(dateStr);
|
|
||||||
if (isNaN(d.getTime())) return String(dateStr);
|
|
||||||
return format(d, "MMM d, yyyy");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
||||||
const { league, sportsSeason, scoringEvent } = loaderData;
|
const { league, sportsSeason, scoringEvent } = loaderData;
|
||||||
const leagueId = league.id;
|
const leagueId = league.id;
|
||||||
|
|
@ -49,9 +42,10 @@ export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventDate = scoringEvent.eventStartsAt
|
const eventDate =
|
||||||
? formatEventDate(scoringEvent.eventStartsAt)
|
formatEventDate(scoringEvent.eventStartsAt as string | null) ??
|
||||||
: formatEventDate(scoringEvent.eventDate);
|
formatEventDate(scoringEvent.eventDate) ??
|
||||||
|
"Date TBD";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-5xl">
|
<div className="container mx-auto py-8 px-4 max-w-5xl">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue