Skip to content
Open
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
7 changes: 7 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import CodeOfConduct from "views/CodeOfConduct";
import Client from "views/Client";
import AdminTournament from "views/AdminTournament";
import { adminTournamentLoader } from "api/loaders/adminTournamentLoader";
import { matchDetailsLoader } from "api/loaders/matchDetailsLoader";
import MatchDetails from "views/MatchDetails";

const queryClient = new QueryClient({
queryCache: new QueryCache({
Expand Down Expand Up @@ -178,6 +180,11 @@ const router = createBrowserRouter([
path: "client",
element: <Client />,
},
{
path: "match/:matchId",
element: <MatchDetails />,
loader: matchDetailsLoader(queryClient),
},
],
},
// Pages that should always be visible
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/api/compete/competeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type PaginatedSubmissionList,
type PaginatedScrimmageRequestList,
type PaginatedMatchList,
type CompeteSubmissionRetrieveRequest,
type CompeteSubmissionCreateRequest,
type CompeteSubmissionDownloadRetrieveRequest,
type CompeteSubmissionListRequest,
Expand All @@ -14,6 +15,8 @@ import {
type CompeteRequestOutboxListRequest,
type CompeteRequestCreateRequest,
type ScrimmageRequest,
type Match,
type CompeteMatchRetrieveRequest,
type CompeteMatchScrimmageListRequest,
type CompeteMatchTournamentListRequest,
type CompeteMatchListRequest,
Expand Down Expand Up @@ -73,6 +76,12 @@ export const downloadSubmission = async ({
await downloadFile(url, `battlecode_source_${id}.zip`);
};

export const getSubmissionInfo = async ({
episodeId,
id,
}: CompeteSubmissionRetrieveRequest): Promise<Submission> =>
await API.competeSubmissionRetrieve({ episodeId, id });

/**
* Get a paginated list of all of the current user's team's submissions.
* @param episodeId The current episode's ID.
Expand Down Expand Up @@ -203,6 +212,15 @@ export const getTournamentMatchesList = async ({
tournamentId,
});

export const getMatchInfo = async ({
episodeId,
id,
}: CompeteMatchRetrieveRequest): Promise<Match> =>
await API.competeMatchRetrieve({
episodeId,
id,
});

/**
* Get all of the matches played in the given episode. Includes both tournament
* matches and scrimmages.
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/api/compete/competeFactories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
CompeteMatchHistoricalRatingTopNListRequest,
CompeteMatchHistoricalRatingRetrieveRequest,
CompeteMatchRetrieveRequest,
CompeteMatchListRequest,
CompeteMatchScrimmageListRequest,
CompeteMatchScrimmagingRecordRetrieveRequest,
Expand All @@ -10,16 +11,20 @@ import type {
CompeteSubmissionListRequest,
CompeteSubmissionTournamentListRequest,
HistoricalRating,
Match,
PaginatedMatchList,
PaginatedScrimmageRequestList,
PaginatedSubmissionList,
TournamentSubmission,
ScrimmageRecord,
CompeteSubmissionRetrieveRequest,
Submission,
} from "../_autogen";
import type { PaginatedQueryFactory, QueryFactory } from "../apiTypes";
import { competeQueryKeys } from "./competeKeys";
import {
getAllUserTournamentSubmissions,
getMatchInfo,
getMatchesList,
getRatingTopNList,
getRatingHistory,
Expand All @@ -29,9 +34,19 @@ import {
getTournamentMatchesList,
getUserScrimmagesInboxList,
getUserScrimmagesOutboxList,
getSubmissionInfo,
} from "./competeApi";
import { prefetchNextPage } from "../helpers";

export const submissionInfoFactory: QueryFactory<
CompeteSubmissionRetrieveRequest,
Submission
> = {
queryKey: competeQueryKeys.subInfo,
queryFn: async ({ episodeId, id }) =>
await getSubmissionInfo({ episodeId, id }),
} as const;

export const subsListFactory: PaginatedQueryFactory<
CompeteSubmissionListRequest,
PaginatedSubmissionList
Expand Down Expand Up @@ -161,6 +176,14 @@ export const teamScrimmageListFactory: PaginatedQueryFactory<
},
} as const;

export const matchInfoFactory: QueryFactory<
CompeteMatchRetrieveRequest,
Match
> = {
queryKey: competeQueryKeys.matchInfo,
queryFn: async ({ episodeId, id }) => await getMatchInfo({ episodeId, id }),
} as const;

export const matchListFactory: PaginatedQueryFactory<
CompeteMatchListRequest,
PaginatedMatchList
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/api/compete/competeKeys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
CompeteMatchHistoricalRatingTopNListRequest,
CompeteMatchHistoricalRatingRetrieveRequest,
CompeteMatchRetrieveRequest,
CompeteMatchListRequest,
CompeteMatchScrimmageListRequest,
CompeteMatchScrimmagingRecordRetrieveRequest,
Expand All @@ -9,12 +10,14 @@ import type {
CompeteRequestOutboxListRequest,
CompeteSubmissionListRequest,
CompeteSubmissionTournamentListRequest,
CompeteSubmissionRetrieveRequest,
} from "../_autogen";
import type { QueryKeyBuilder } from "../apiTypes";

interface CompeteKeys {
// --- SUBMISSIONS --- //
subBase: QueryKeyBuilder<{ episodeId: string }>;
subInfo: QueryKeyBuilder<CompeteSubmissionRetrieveRequest>;
subList: QueryKeyBuilder<CompeteSubmissionListRequest>;
tourneySubs: QueryKeyBuilder<CompeteSubmissionTournamentListRequest>;
// --- SCRIMMAGES --- //
Expand All @@ -25,6 +28,7 @@ interface CompeteKeys {
scrimsOtherList: QueryKeyBuilder<CompeteMatchScrimmageListRequest>;
// --- MATCHES --- //
matchBase: QueryKeyBuilder<{ episodeId: string }>;
matchInfo: QueryKeyBuilder<CompeteMatchRetrieveRequest>;
matchList: QueryKeyBuilder<CompeteMatchListRequest>;
tourneyMatchList: QueryKeyBuilder<CompeteMatchTournamentListRequest>;
// --- PERFORMANCE --- //
Expand All @@ -43,6 +47,11 @@ export const competeQueryKeys: CompeteKeys = {
["compete", episodeId, "submissions"] as const,
},

subInfo: {
key: ({ episodeId, id }: CompeteSubmissionRetrieveRequest) =>
[...competeQueryKeys.subBase.key({ episodeId }), "info", id] as const,
},

subList: {
key: ({ episodeId, page = 1 }: CompeteSubmissionListRequest) =>
[...competeQueryKeys.subBase.key({ episodeId }), "list", page] as const,
Expand Down Expand Up @@ -103,6 +112,11 @@ export const competeQueryKeys: CompeteKeys = {
["compete", episodeId, "matches"] as const,
},

matchInfo: {
key: ({ episodeId, id }: CompeteMatchRetrieveRequest) =>
[...competeQueryKeys.matchBase.key({ episodeId }), "info", id] as const,
},

matchList: {
key: ({ episodeId, page = 1 }: CompeteMatchListRequest) =>
[...competeQueryKeys.matchBase.key({ episodeId }), "list", page] as const,
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/api/compete/useCompete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { competeMutationKeys, competeQueryKeys } from "./competeKeys";
import type {
CompeteMatchHistoricalRatingTopNListRequest,
CompeteMatchHistoricalRatingRetrieveRequest,
CompeteMatchRetrieveRequest,
CompeteMatchListRequest,
CompeteMatchScrimmageListRequest,
CompeteMatchScrimmagingRecordRetrieveRequest,
Expand All @@ -20,10 +21,12 @@ import type {
CompeteRequestOutboxListRequest,
CompeteRequestRejectCreateRequest,
CompeteSubmissionCreateRequest,
CompeteSubmissionRetrieveRequest,
CompeteSubmissionListRequest,
CompeteSubmissionTournamentListRequest,
CompeteSubmissionDownloadRetrieveRequest,
HistoricalRating,
Match,
PaginatedMatchList,
PaginatedScrimmageRequestList,
PaginatedSubmissionList,
Expand All @@ -44,6 +47,7 @@ import {
import toast from "react-hot-toast";
import { buildKey } from "../helpers";
import {
matchInfoFactory,
matchListFactory,
ratingHistoryTopNFactory,
userRatingHistoryFactory,
Expand All @@ -56,12 +60,22 @@ import {
tournamentSubsListFactory,
userScrimmageListFactory,
ratingHistoryFactory,
submissionInfoFactory,
} from "./competeFactories";
import { MILLIS_SECOND, SECONDS_MINUTE } from "utils/utilTypes";

// ---------- QUERY HOOKS ---------- //
const STATISTICS_WAIT_MINUTES = 5;

export const useSubmissionInfo = ({
episodeId,
id,
}: CompeteSubmissionRetrieveRequest): UseQueryResult<Submission> =>
useQuery({
queryKey: buildKey(submissionInfoFactory.queryKey, { episodeId, id }),
queryFn: async () => await submissionInfoFactory.queryFn({ episodeId, id }),
});

/**
* For retrieving a list of the currently logged in user's submissions.
*/
Expand Down Expand Up @@ -163,6 +177,15 @@ export const useTeamScrimmageList = (
),
});

export const useMatchInfo = ({
episodeId,
id,
}: CompeteMatchRetrieveRequest): UseQueryResult<Match> =>
useQuery({
queryKey: buildKey(matchInfoFactory.queryKey, { episodeId, id }),
queryFn: async () => await matchInfoFactory.queryFn({ episodeId, id }),
});

/**
* For retrieving a paginated list of the matches in a given episode.
*/
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/api/loaders/matchDetailsLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { QueryClient } from "@tanstack/react-query";
import { matchInfoFactory } from "api/compete/competeFactories";
import { safeEnsureQueryData } from "api/helpers";
import type { LoaderFunction } from "react-router-dom";
import { isPresent } from "utils/utilTypes";

export const matchDetailsLoader =
(queryClient: QueryClient): LoaderFunction =>
({ params }) => {
const { episodeId, id } = params;

if (!isPresent(id) || !isPresent(episodeId)) return null;

// Load match info
safeEnsureQueryData(
{
episodeId,
id,
},
matchInfoFactory,
queryClient,
);

return null;
};
16 changes: 12 additions & 4 deletions frontend/src/api/team/useTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
import { buildKey } from "../helpers";
import { userRatingHistoryFactory } from "api/compete/competeFactories";
import { competeQueryKeys } from "api/compete/competeKeys";
import { MILLIS_SECOND } from "utils/utilTypes";
import { type Maybe, MILLIS_SECOND } from "utils/utilTypes";

// ---------- QUERY HOOKS ---------- //
const SEARCH_WAIT_SECONDS = 30;
Expand Down Expand Up @@ -129,14 +129,17 @@ export const useCreateTeam = (
export const useJoinTeam = (
{ episodeId }: { episodeId: string },
queryClient: QueryClient,
): UseMutationResult<void, Error, TeamJoinRequest> =>
useMutation({
): UseMutationResult<void, Error, TeamJoinRequest> => {
let err: Maybe<string> = undefined;
const getErr = (): string => err ?? "Error joining team.";

return useMutation({
mutationKey: teamMutationKeys.join({ episodeId }),
mutationFn: async (teamJoinRequest: TeamJoinRequest) => {
await toast.promise(joinTeam({ episodeId, teamJoinRequest }), {
loading: "Joining team...",
success: "Joined team!",
error: "Error joining team.",
error: getErr,
});
},
onSuccess: async () => {
Expand All @@ -153,7 +156,12 @@ export const useJoinTeam = (
queryKey: competeQueryKeys.scrimBase.key({ episodeId }),
});
},
onError: (error) => {
err = `${error.name}: ${error.message}`;
console.log(err);
},
});
};

/**
* Leave the user's current team in a given episode.
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/MatchReplayButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ const MatchReplayButton: React.FC<MatchReplayButtonProps> = ({
<Button
disabled={disabled || !isPresent(clientUrl)}
label="Replay!"
onClick={() => {
onClick={(e) => {
if (isPresent(clientUrl)) {
window.open(clientUrl);
}
e.stopPropagation();
}}
/>
);
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/compete/TeamWithRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ const TeamWithRating: React.FC<TeamWithRatingProps> = ({

return (
<>
<NavLink to={`/${episodeId}/team/${teamId}`} className="hover:underline">
<NavLink
to={`/${episodeId}/team/${teamId}`}
className="hover:underline"
onClick={(e) => {
e.stopPropagation();
}}
>
{includeTeamName && <span>{teamName}</span>}
{ratingComponent}
</NavLink>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useEpisodeId } from "contexts/EpisodeContext";
import { useUserTeam } from "api/team/useTeam";
import { useUserScrimmageList } from "api/compete/useCompete";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import MatchReplayButton from "components/MatchReplayButton";

interface ScrimHistoryTableProps {
Expand All @@ -24,6 +25,7 @@ const ScrimHistoryTable: React.FC<ScrimHistoryTableProps> = ({
}) => {
const { episodeId } = useEpisodeId();
const queryClient = useQueryClient();
const navigate = useNavigate();
const episodeData = useEpisodeInfo({ id: episodeId });
const userTeamData = useUserTeam({ episodeId });
const scrimsData = useUserScrimmageList(
Expand Down Expand Up @@ -113,6 +115,9 @@ const ScrimHistoryTable: React.FC<ScrimHistoryTableProps> = ({
value: (match) => dateTime(match.created).localFullString,
},
]}
onRowClick={(match) => {
navigate(`/${episodeId}/match/${match.id.toString()}`);
}}
/>
</Fragment>
);
Expand Down
Loading
Loading