Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/app/events/[event_key]/EventKeyClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
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";

Check warning on line 22 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'WebcastUrl' is defined but never used

Check warning on line 22 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'EventWebcastType' is defined but never used

Check warning on line 22 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'WebcastUrl' is defined but never used

Check warning on line 22 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'EventWebcastType' is defined but never used
import EventOverview from "./EventOverviewTab";
import EventMatchesTab from "./EventMatchesTab";
import EventTeamsTab from "./EventTeamsTab";
import EventRankingsTab from "./EventRankingsTab";
import EventAwardsTab from "./EventAwardsTab";
import { EventInfoProps } from "@/components/events/event/EventInfo";

Check warning on line 28 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'EventInfoProps' is defined but never used

Check warning on line 28 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'EventInfoProps' is defined but never used
import { getTeamSummary } from "@/lib/api/teams";

Check warning on line 29 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'getTeamSummary' is defined but never used

Check warning on line 29 in src/app/events/[event_key]/EventKeyClient.tsx

View workflow job for this annotation

GitHub Actions / build

'getTeamSummary' is defined but never used


interface EventKeyProps {
Expand All @@ -36,15 +36,16 @@
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 <EventOverview event={event} />

case EventTab.Matches:
return <EventMatchesTab />;
return <EventMatchesTab matches={matchs ? matchs : []} loading={false} />;

case EventTab.Teams:
return <EventTeamsTab teams={teams ? teams : []} loading={false} />;
Expand Down Expand Up @@ -91,11 +92,11 @@
}
}

export default function EventKeyClient({ eventKey, tab, event, teams, rankings, awards}: EventKeyProps) {
export default function EventKeyClient({ eventKey, tab, event, teams, rankings, awards, matchs}: EventKeyProps) {
return (
<main className="mx-auto flex w-full max-w-7xl flex-col gap-6 overflow-x-hidden px-4 py-6 sm:px-6">
<EventHeader eventKey={eventKey} activeTab={tab} event={event} />
<EventTabContent eventKey={eventKey} tab={tab} event={event} teams={teams} rankings={rankings} awards={awards} />
<EventTabContent eventKey={eventKey} tab={tab} event={event} teams={teams} rankings={rankings} awards={awards} matchs={matchs} />
</main>
);
}
81 changes: 68 additions & 13 deletions src/app/events/[event_key]/EventMatchesTab.tsx
Original file line number Diff line number Diff line change
@@ -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:
*
* <EventMatchesTab matches={event.matches} loading={false} />
* 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";

Expand All @@ -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<MatchBracket>("qualification");

const { qualification, playoff } = useMemo(() => {
const grouped: Record<MatchBracket, EventMatch[]> = {
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 (
<div className="space-y-6">
Expand All @@ -42,19 +71,45 @@ export default function EventMatchesTab({ matches = [], loading = true }: EventM
</h2>

<div className="flex gap-2">
<span className="rounded-lg bg-blue-600/20 px-3 py-1.5 text-xs font-medium text-blue-400">
Qualification
</span>
{BRACKETS.map(({ value, label }) => {
const isActive = bracket === value;
const count = value === "qualification" ? qualification.length : playoff.length;

<span className="rounded-lg px-3 py-1.5 text-xs font-medium text-slate-500">
Playoffs
</span>
return (
<button
key={value}
type="button"
onClick={() => setBracket(value)}
aria-pressed={isActive}
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition ${
isActive
? "bg-blue-600/20 text-blue-400"
: "text-slate-500 hover:text-slate-300"
}`}
>
{label}
{!loading && !isEmpty && (
<span className="ml-1.5 text-slate-500">
{count}
</span>
)}
</button>
);
})}
</div>
</div>

{!isEmpty && (
<div className="mt-5">
<MatchesList matches={matches} loading={loading} />
{isBracketEmpty ? (
<p className="rounded-lg border border-dashed border-slate-800 bg-slate-950 p-4 text-sm text-slate-400">
{bracket === "qualification"
? "No qualification matches yet."
: "The playoff bracket hasn't started yet."}
</p>
) : (
<MatchesList matches={visibleMatches} loading={loading} />
)}
</div>
)}
</section>
Expand Down
5 changes: 4 additions & 1 deletion src/app/events/[event_key]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand All @@ -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) {
Expand All @@ -68,6 +70,7 @@ export default async function EventPage({
tab={validTab}
awards={awards}
teams={teams}
matchs={matchs}
/>
);
}
4 changes: 2 additions & 2 deletions src/components/cards/MatchRowCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/dashboard/UpcomingMatches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[];
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/events/event/matches/MatchesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? []}
/>
))}
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/TeamBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
76 changes: 75 additions & 1 deletion src/lib/api/match.ts
Original file line number Diff line number Diff line change
@@ -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";

Check warning on line 5 in src/lib/api/match.ts

View workflow job for this annotation

GitHub Actions / build

'promises' is defined but never used

Check warning on line 5 in src/lib/api/match.ts

View workflow job for this annotation

GitHub Actions / build

'promises' is defined but never used

/* -------------------------------------------------------------------------- */
/* Constants */
Expand Down Expand Up @@ -115,7 +118,7 @@
* 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
Expand Down Expand Up @@ -145,3 +148,74 @@
})
);
}


/**
* 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<EventAllianceTeam[] | null> {
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<EventMatch[] | null> {
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),
compLevel: match.comp_level,
// `actual_time` is set as soon as the match is *played*, but TBA
// only finishes scoring/reviewing it at `post_result_time` — using
// `actual_time` alone was marking every played-but-not-yet-reviewed
// match as COMPLETED ("Final") too early. `post_result_time` is the
// real "this match is done" signal; `actual_time` alone just means
// it's on the field / awaiting result.
status: match.post_result_time
? MatchStatus.COMPLETED
: match.actual_time
? MatchStatus.ON_FIELD
: 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;
}
}
26 changes: 18 additions & 8 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* App-level shapes consumed by `/events` and `/events/[event_key]`.
* Adapted from raw `TBAEvent*` payloads by `src/lib/api/events.ts`.
*/
import { CompetitionLevel } from "@/types/tba/common";

export enum EventWebcastType {
YouTube = "youtube",
Twitch = "twitch",
Expand Down Expand Up @@ -90,23 +92,31 @@ 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;
/** TBA `comp_level` ("qm" | "ef" | "qf" | "sf" | "f") — drives the
* Qualification / Playoffs split on the Matches tab. */
compLevel: CompetitionLevel | 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. */
Expand Down
Loading
Loading