From 2f06e548e9b3e0a941e4ea7c33daf5afa3ee2d7f Mon Sep 17 00:00:00 2001 From: Kaique Date: Sat, 15 Aug 2026 17:15:11 -0300 Subject: [PATCH 1/2] Add event match fetching and MatchStatus Introduce MatchStatus enum and extend event types to support nullable match/alliance fields. Add getMatchsEvent (and helpers) to fetch and map TBA matches (uses formatMatchLabel) and export getAllianceTeams. Update page and client to load/passthrough matches, and adjust MatchRowCard, MatchesList, UpcomingMatches, and TeamBadge props to handle nullable teams/status. These changes enable fetching/displaying event matches and make match-related props more resilient to missing data. --- src/app/events/[event_key]/EventKeyClient.tsx | 11 ++-- src/app/events/[event_key]/page.tsx | 5 +- src/components/cards/MatchRowCard.tsx | 4 +- src/components/dashboard/UpcomingMatches.tsx | 3 +- .../events/event/matches/MatchesList.tsx | 4 +- src/components/ui/TeamBadge.tsx | 6 +- src/lib/api/match.ts | 65 ++++++++++++++++++- src/types/events.ts | 21 +++--- 8 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/app/events/[event_key]/EventKeyClient.tsx b/src/app/events/[event_key]/EventKeyClient.tsx index 5cd07aa..32a2f6e 100644 --- a/src/app/events/[event_key]/EventKeyClient.tsx +++ b/src/app/events/[event_key]/EventKeyClient.tsx @@ -19,7 +19,7 @@ import Link from "next/link"; import { ArrowRight, ClipboardList } from "lucide-react"; import EventHeader from "@/components/events/event/EventHeader"; -import { EventAward, EventFull, EventRankingRow, EventTab, EventTeamSummary, EventWebcastType, WebcastUrl} from "@/types/events"; +import { EventAward, EventFull, EventMatch, EventRankingRow, EventTab, EventTeamSummary, EventWebcastType, WebcastUrl} from "@/types/events"; import EventOverview from "./EventOverviewTab"; import EventMatchesTab from "./EventMatchesTab"; import EventTeamsTab from "./EventTeamsTab"; @@ -36,15 +36,16 @@ interface EventKeyProps { teams: EventTeamSummary[] | null; rankings: EventRankingRow[] | null; awards: EventAward[] | null; + matchs: EventMatch[] | null; } -async function EventTabContent({ eventKey, tab, event, teams, rankings, awards }: EventKeyProps) { +async function EventTabContent({ eventKey, tab, event, teams, rankings, awards, matchs }: EventKeyProps) { switch (tab) { case EventTab.Overview: return case EventTab.Matches: - return ; + return ; case EventTab.Teams: return ; @@ -91,11 +92,11 @@ async function EventTabContent({ eventKey, tab, event, teams, rankings, awards } } } -export default function EventKeyClient({ eventKey, tab, event, teams, rankings, awards}: EventKeyProps) { +export default function EventKeyClient({ eventKey, tab, event, teams, rankings, awards, matchs}: EventKeyProps) { return (
- +
); } diff --git a/src/app/events/[event_key]/page.tsx b/src/app/events/[event_key]/page.tsx index 1b1945a..5256958 100644 --- a/src/app/events/[event_key]/page.tsx +++ b/src/app/events/[event_key]/page.tsx @@ -21,6 +21,7 @@ import EventKeyClient from "./EventKeyClient"; import { EventTab } from "@/types/events"; import { getEventAwards, getEventFull, getEventRankings } from "@/lib/api/events"; import { getTeamSummary } from "@/lib/api/teams"; +import { getMatchsEvent } from "@/lib/api/match"; interface EventPageProps { params: Promise<{ @@ -43,11 +44,12 @@ export default async function EventPage({ // every load wait for two full sequential round-trips instead of one. // (The bigger cost is inside `getTeamSummary` itself, which is // off-limits here — see the suggestions shared alongside this change.) - const [event, teams, rankings, awards] = await Promise.all([ + const [event, teams, rankings, awards, matchs] = await Promise.all([ getEventFull(event_key), getTeamSummary(event_key), getEventRankings(event_key), getEventAwards(event_key), + getMatchsEvent(event_key), ]); if (!event) { @@ -68,6 +70,7 @@ export default async function EventPage({ tab={validTab} awards={awards} teams={teams} + matchs={matchs} /> ); } diff --git a/src/components/cards/MatchRowCard.tsx b/src/components/cards/MatchRowCard.tsx index a71f1a2..89fdb02 100644 --- a/src/components/cards/MatchRowCard.tsx +++ b/src/components/cards/MatchRowCard.tsx @@ -10,8 +10,8 @@ import TeamBadge from "@/components/ui/TeamBadge"; import { EventAllianceTeam, MatchStatus } from "@/types/events"; interface MatchRowProps { - match: string; - status?: MatchStatus; + match: string | null; + status?: MatchStatus | null; red: EventAllianceTeam[]; blue: EventAllianceTeam[]; } diff --git a/src/components/dashboard/UpcomingMatches.tsx b/src/components/dashboard/UpcomingMatches.tsx index ea6d4a1..30a5366 100644 --- a/src/components/dashboard/UpcomingMatches.tsx +++ b/src/components/dashboard/UpcomingMatches.tsx @@ -7,6 +7,7 @@ * there's no per-match alliance/status API yet. */ import MatchRow from "@/components/cards/MatchRowCard"; +import { MatchStatus } from "@/types/events"; interface MatchTeam { team: number; @@ -16,7 +17,7 @@ interface MatchTeam { export interface Match { key: string; match: string; - status: "live" | "on_field" | "scheduled"; + status: MatchStatus | null; red: MatchTeam[]; blue: MatchTeam[]; } diff --git a/src/components/events/event/matches/MatchesList.tsx b/src/components/events/event/matches/MatchesList.tsx index 19cd973..d673d5f 100644 --- a/src/components/events/event/matches/MatchesList.tsx +++ b/src/components/events/event/matches/MatchesList.tsx @@ -27,8 +27,8 @@ export default function MatchesList({ matches, loading }: MatchesListProps) { key={match.key} match={match.match} status={match.status} - red={match.red} - blue={match.blue} + red={match.red ?? []} + blue={match.blue ?? []} /> ))} diff --git a/src/components/ui/TeamBadge.tsx b/src/components/ui/TeamBadge.tsx index 33575e5..6f1701b 100644 --- a/src/components/ui/TeamBadge.tsx +++ b/src/components/ui/TeamBadge.tsx @@ -4,9 +4,9 @@ import Link from "next/link"; import { Star } from "lucide-react"; interface TeamBadgeProps { - team: number; - alliance: "red" | "blue"; - favorite?: boolean; + team: number | null; + alliance: "red" | "blue" | null; + favorite?: boolean | null; } export default function TeamBadge({ diff --git a/src/lib/api/match.ts b/src/lib/api/match.ts index 82ac907..6b73cf7 100644 --- a/src/lib/api/match.ts +++ b/src/lib/api/match.ts @@ -1,5 +1,8 @@ import * as tba from "@/lib/api/tba"; +import { EventAllianceTeam, EventMatch, MatchStatus } from "@/types/events"; import { AllianceTeam, MatchAlliances, MatchOption } from "@/types/scout"; +import { formatMatchLabel } from "@/utils/match"; +import { promises } from "node:timers"; /* -------------------------------------------------------------------------- */ /* Constants */ @@ -115,7 +118,7 @@ export async function getMatchAlliances( * depend on each other — and pairs each team with its station label by * array position (see `RED_STATIONS` / `BLUE_STATIONS` above). */ -async function getAllianceTeams( +export async function getAllianceTeams( teamKeys: string[], stations: readonly string[], year: number @@ -145,3 +148,63 @@ async function getAllianceTeams( }) ); } + + +/** + * Teamskeys come in `sting[]` and it returns `EventAllianceTeam[]` + * + * @TODO: favorite boolean is always false, test favorite will be add soon + * + * @param Teams string array from teams keys it's converted in `number[]` + * to use on EventAllianceTeam type + * + * @returns `EventAllianceTeam[]` [team: number(team number), favorite: boolean] + */ +async function getEventAlliancesTeams( + Teams: string[] +): Promise { + try { + const allianceTeams: EventAllianceTeam[] = [] + for(const team of Teams) + { + allianceTeams.push({ + team: Number(team.replace("frc", "")), favorite: false, + }) + } + + return allianceTeams; + } catch { + return null; + } +} + +/** + * fetchs all matchs from an event + * @param eventKey + * @returns + */ +export async function getMatchsEvent(eventKey: string) : Promise { + try { + const matchs = await tba.getEventMatches(eventKey); + + const eventMatchs: EventMatch[] = []; + + for (const match of matchs) + { + eventMatchs.push({ + key: match.key, + match: formatMatchLabel(match.comp_level, match.match_number, match.key), + status: match.actual_time ? MatchStatus.COMPLETED : MatchStatus.SCHEDULED, + red: await getEventAlliancesTeams(match.alliances.red.team_keys), + blue: await getEventAlliancesTeams(match.alliances.blue.team_keys), + redScore: match.alliances.red.score, + blueScore: match.alliances.blue.score, + scheduledTime: `${match.predicted_time}`, + }); + } + + return eventMatchs; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/types/events.ts b/src/types/events.ts index 27eafac..56e4e6d 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -90,23 +90,28 @@ export enum EventTab { /** One team's badge within a match alliance. Mirrors `MatchRowCard`'s team shape. */ export interface EventAllianceTeam { - team: number; - favorite?: boolean; + team: number | null; + favorite?: boolean| null; } /** Match status shown as the pill on a match row. */ -export type MatchStatus = "scheduled" | "on_field" | "live" | "completed"; +export enum MatchStatus { + SCHEDULED = "scheduled", + ON_FIELD = "on_field", + LIVE = "live", + COMPLETED = "completed", +} /** A single match row for the Matches tab — ready for `MatchRowCard`. */ export interface EventMatch { key: string; - match: string; - status: MatchStatus; - red: EventAllianceTeam[]; - blue: EventAllianceTeam[]; + match: string | null; + status: MatchStatus | null; + red: EventAllianceTeam[] | null; + blue: EventAllianceTeam[] | null; redScore?: number | null; blueScore?: number | null; - scheduledTime?: string | null; + scheduledTime: string | null; } /** A single team entry for the Teams tab grid. */ From c43eed79e12cdf998db9ff105ce24540599f412a Mon Sep 17 00:00:00 2001 From: Kaique Date: Sat, 15 Aug 2026 18:04:52 -0300 Subject: [PATCH 2/2] Split matches by bracket, sort order, and status fix Make Matches tab interactive and accurate: EventMatchesTab is now a client component with a Qualification/Playoff toggle, groups matches by comp_level and shows counts, and displays matches sorted by execution order. Add utilities getMatchBracket, sortMatchesByExecutionOrder and matchNumberFromKey (utils/match). Update EventMatch type to include compLevel (types/events). Fix match status logic to use post_result_time (ON_FIELD vs COMPLETED) and tweak label parsing (lib/api/match, utils/match). --- .../events/[event_key]/EventMatchesTab.tsx | 81 ++++++++++++++++--- src/lib/api/match.ts | 13 ++- src/types/events.ts | 5 ++ src/utils/match.ts | 70 ++++++++++++++++ 4 files changed, 155 insertions(+), 14 deletions(-) diff --git a/src/app/events/[event_key]/EventMatchesTab.tsx b/src/app/events/[event_key]/EventMatchesTab.tsx index 0e44308..2ce837a 100644 --- a/src/app/events/[event_key]/EventMatchesTab.tsx +++ b/src/app/events/[event_key]/EventMatchesTab.tsx @@ -1,15 +1,19 @@ /** * EventMatchesTab * - * Matches tab for `/events/[event_key]`. Defaults to `loading = true` - * (skeleton) since there's no real data source wired up yet — once - * the TBA matches endpoint is adapted into `EventMatch[]` - * (`src/types/events.ts`), pass it in: - * - * + * Matches tab for `/events/[event_key]`. Splits `matches` into + * Qualification / Playoff brackets (via `getMatchBracket`, based on TBA's + * `comp_level`) behind a toggle — that toggle used to be two static, + * non-interactive pills; it's now real state. Within the active bracket, + * matches are sorted into the order they're actually run + * (`sortMatchesByExecutionOrder`). */ +"use client"; + +import { useMemo, useState } from "react"; import { Swords } from "lucide-react"; import { EventMatch } from "@/types/events"; +import { getMatchBracket, sortMatchesByExecutionOrder, MatchBracket } from "@/utils/match"; import MatchesList from "@/components/events/event/matches/MatchesList"; import EmptyTabState from "./EmptyTabState"; @@ -18,8 +22,33 @@ interface EventMatchesTabProps { loading?: boolean; } +const BRACKETS: { value: MatchBracket; label: string }[] = [ + { value: "qualification", label: "Qualification" }, + { value: "playoff", label: "Playoffs" }, +]; + export default function EventMatchesTab({ matches = [], loading = true }: EventMatchesTabProps) { + const [bracket, setBracket] = useState("qualification"); + + const { qualification, playoff } = useMemo(() => { + const grouped: Record = { + qualification: [], + playoff: [], + }; + + for (const match of matches) { + grouped[getMatchBracket(match.compLevel)].push(match); + } + + return { + qualification: sortMatchesByExecutionOrder(grouped.qualification), + playoff: sortMatchesByExecutionOrder(grouped.playoff), + }; + }, [matches]); + + const visibleMatches = bracket === "qualification" ? qualification : playoff; const isEmpty = !loading && matches.length === 0; + const isBracketEmpty = !loading && !isEmpty && visibleMatches.length === 0; return (
@@ -42,19 +71,45 @@ export default function EventMatchesTab({ matches = [], loading = true }: EventM
- - Qualification - + {BRACKETS.map(({ value, label }) => { + const isActive = bracket === value; + const count = value === "qualification" ? qualification.length : playoff.length; - - Playoffs - + return ( + + ); + })}
{!isEmpty && (
- + {isBracketEmpty ? ( +

+ {bracket === "qualification" + ? "No qualification matches yet." + : "The playoff bracket hasn't started yet."} +

+ ) : ( + + )}
)} diff --git a/src/lib/api/match.ts b/src/lib/api/match.ts index 6b73cf7..914b1d6 100644 --- a/src/lib/api/match.ts +++ b/src/lib/api/match.ts @@ -194,7 +194,18 @@ export async function getMatchsEvent(eventKey: string) : Promise = { + qm: 0, + ef: 1, + qf: 2, + sf: 3, + f: 4, +}; + +/** The two groups the Matches tab splits into: everything that isn't a + * qualification match ("qm") is a playoff match ("ef" / "qf" / "sf" / "f"). */ +export type MatchBracket = "qualification" | "playoff"; + +export function getMatchBracket( + compLevel: CompetitionLevel | null | undefined +): MatchBracket { + return compLevel === "qm" ? "qualification" : "playoff"; +} + +/** + * Sorts matches in the order they're actually run at the event: by + * `scheduledTime` first (the real chronological order TBA predicts/records + * for the match), falling back to comp level + match number for matches + * that don't have a time yet (so a freshly-generated playoff bracket still + * renders in bracket order instead of jumping around). + */ +export function sortMatchesByExecutionOrder(matches: T[]): T[] { + return [...matches].sort((a, b) => { + const aTime = a.scheduledTime ? Number(a.scheduledTime) : null; + const bTime = b.scheduledTime ? Number(b.scheduledTime) : null; + + const aValid = aTime !== null && !Number.isNaN(aTime); + const bValid = bTime !== null && !Number.isNaN(bTime); + + if (aValid && bValid && aTime !== bTime) { + return (aTime as number) - (bTime as number); + } + + // No usable time on one/both sides — fall back to bracket order. + if (aValid !== bValid) { + return aValid ? -1 : 1; + } + + const aLevel = a.compLevel ? COMP_LEVEL_ORDER[a.compLevel] : 0; + const bLevel = b.compLevel ? COMP_LEVEL_ORDER[b.compLevel] : 0; + + if (aLevel !== bLevel) { + return aLevel - bLevel; + } + + return matchNumberFromKey(a.key) - matchNumberFromKey(b.key); + }); +} + +/** Pulls the trailing numeric part out of a match key (`"...qm12"` -> `12`, + * `"...sf1m2"` -> `2`) for use as a last-resort sort tiebreaker. */ +function matchNumberFromKey(key: string | null | undefined): number { + if (!key) return 0; + const digits = key.match(/(\d+)(?!.*\d)/); + return digits ? Number(digits[1]) : 0; } \ No newline at end of file