diff --git a/next-frontend/public/wcaAPI.yaml b/next-frontend/public/wcaAPI.yaml
index 0d0abaf6c0d..c488a2c6cad 100644
--- a/next-frontend/public/wcaAPI.yaml
+++ b/next-frontend/public/wcaAPI.yaml
@@ -81,6 +81,32 @@ paths:
type: array
items:
$ref: '#/components/schemas/RegistrationData'
+ /competitions/{competitionId}/psych-sheet/{eventId}:
+ get:
+ summary: Get competition registrations
+ parameters:
+ - name: competitionId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: eventId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: sort_by
+ in: query
+ required: false
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PsychSheet'
/competitions/{competitionId}/podiums:
get:
summary: Returns the podium results
@@ -1337,24 +1363,123 @@ components:
name:
type: string
- RegistrationData:
+ PsychSheet:
type: object
+ required:
+ - sort_by
+ - sort_by_second
+ - sorted_rankings
properties:
- id:
- type: integer
- competition_id:
+ sort_by:
type: string
- user_id:
- type: integer
- event_ids:
+ sort_by_second:
+ type: string
+ sorted_rankings:
type: array
items:
- type: string
+ type: object
+ required:
+ - name
+ - user_id
+ - wca_id
+ - country_iso2
+ - average_best
+ - average_rank
+ - single_best
+ - single_rank
+ - tied_previous
+ - pos
+ properties:
+ name:
+ type: string
+ user_id:
+ type: integer
+ wca_id:
+ type: string
+ country_iso2:
+ type: string
+ average_best:
+ type: integer
+ average_rank:
+ type: integer
+ single_best:
+ type: integer
+ single_rank:
+ type: integer
+ tied_previous:
+ type: boolean
+ pos:
+ type: integer
+
+ RegistrationData:
+ type: object
required:
- id
- - competition_id
+ - registrant_id
- user_id
- - event_ids
+ - user
+ - competing
+ properties:
+ id:
+ type: integer
+ registrant_id:
+ type: integer
+ user_id:
+ type: integer
+ guests:
+ type: integer
+ user:
+ type: object
+ required:
+ - id
+ - name
+ - gender
+ - country_iso2
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ gender:
+ type: string
+ country_iso2:
+ type: string
+ wca_id:
+ type: string
+ competing:
+ type: object
+ required:
+ - event_ids
+ properties:
+ event_ids:
+ type: array
+ items:
+ type: string
+ # only when authenticated
+ registration_status:
+ type: string
+ registered_on:
+ type: string
+ format: datetime
+ comment:
+ type: string
+ admin_comment:
+ type: string
+ # only when authenticated
+ payment:
+ type: object
+ properties:
+ has_paid:
+ type: boolean
+ payment_status:
+ type: string
+ paid_amount_iso:
+ type: integer
+ currency_code:
+ type: string
+ updated_at:
+ type: string
+ format: datetime
WcifEvent:
type: object
diff --git a/next-frontend/src/components/competitions/CompetitorTable.tsx b/next-frontend/src/components/competitions/CompetitorTable.tsx
new file mode 100644
index 00000000000..1703d35dac7
--- /dev/null
+++ b/next-frontend/src/components/competitions/CompetitorTable.tsx
@@ -0,0 +1,84 @@
+import { HStack, Icon, Link, Table, Text } from "@chakra-ui/react";
+import EventIcon from "@/components/EventIcon";
+import { route } from "nextjs-routes";
+import Flag from "react-world-flags";
+import CountryMap from "@/components/CountryMap";
+import { components } from "@/types/openapi";
+import { TFunction } from "i18next";
+
+export default function CompetitorTable({
+ eventIds,
+ registrations,
+ t,
+ setPsychSheetEvent,
+}: {
+ eventIds: string[];
+ registrations: components["schemas"]["RegistrationData"][];
+ setPsychSheetEvent: (eventId: string) => void;
+ t: TFunction;
+}) {
+ return (
+
+
+
+ Name
+ Representing
+ {eventIds.map((eventId) => (
+ setPsychSheetEvent(eventId)}
+ _hover={{ bg: "grey.solid", color: "wcawhite.contrast" }}
+ >
+
+
+ ))}
+ Total
+
+
+
+
+ {registrations
+ .toSorted((a, b) => a.user.name.localeCompare(b.user.name))
+ .map((registration) => (
+
+
+ {registration.user.wca_id ? (
+
+ {registration.user.name}
+
+ ) : (
+ {registration.user.name}
+ )}
+
+
+
+
+
+
+
+
+
+
+ {eventIds.map((eventId) => (
+
+ {registration.competing.event_ids.includes(eventId) ? (
+
+ ) : null}
+
+ ))}
+ {registration.competing.event_ids.length}
+
+ ))}
+
+
+ );
+}
diff --git a/next-frontend/src/components/competitions/PsychsheetTable.tsx b/next-frontend/src/components/competitions/PsychsheetTable.tsx
new file mode 100644
index 00000000000..6449e32d034
--- /dev/null
+++ b/next-frontend/src/components/competitions/PsychsheetTable.tsx
@@ -0,0 +1,65 @@
+import { HStack, Icon, Link, Table, Text } from "@chakra-ui/react";
+import { route } from "nextjs-routes";
+import Flag from "react-world-flags";
+import CountryMap from "@/components/CountryMap";
+import { components } from "@/types/openapi";
+import { TFunction } from "i18next";
+
+export default function PsychsheetTable({
+ pychsheet,
+ t,
+}: {
+ pychsheet: components["schemas"]["PsychSheet"];
+ t: TFunction;
+}) {
+ return (
+
+
+
+ Pos
+ Name
+ Representing
+ WR
+ Single
+ Average
+ WR
+
+
+
+
+ {pychsheet.sorted_rankings
+ .toSorted((a, b) => a.pos - b.pos)
+ .map(
+ (registration) =>
+ registration.wca_id && (
+
+ {registration.pos}
+
+
+ {registration.name}
+
+
+
+
+
+
+
+
+
+
+ {registration.single_rank}
+ {registration.single_best}
+ {registration.average_best}
+ {registration.average_rank}
+
+ ),
+ )}
+
+
+ );
+}
diff --git a/next-frontend/src/components/competitions/TabCompetitors.tsx b/next-frontend/src/components/competitions/TabCompetitors.tsx
index 53dc6368744..c398cb50f81 100644
--- a/next-frontend/src/components/competitions/TabCompetitors.tsx
+++ b/next-frontend/src/components/competitions/TabCompetitors.tsx
@@ -1,18 +1,36 @@
"use client";
-import React, { useMemo } from "react";
-import { Card, Text, Table, Center, Spinner } from "@chakra-ui/react";
+import React, { useMemo, useState } from "react";
+import {
+ Card,
+ Text,
+ Table,
+ Center,
+ Spinner,
+ Link,
+ HStack,
+ Icon,
+} from "@chakra-ui/react";
import EventIcon from "@/components/EventIcon";
import CountryMap from "@/components/CountryMap";
import { useQuery } from "@tanstack/react-query";
import useAPI from "@/lib/wca/useAPI";
import { useT } from "@/lib/i18n/useI18n";
+import { route } from "nextjs-routes";
+import Flag from "react-world-flags";
+import EventSelector from "@/components/EventSelector";
+import CompetitorTable from "@/components/competitions/CompetitorTable";
+import PsychsheetTable from "@/components/competitions/PsychsheetTable";
interface CompetitorData {
id: string;
}
const TabCompetitors: React.FC = ({ id }) => {
- const api = useAPI();
+ const [psychSheetEvent, setPsychSheetEvent] = useState(null);
+ const [sortBy, setSortBy] = useState("average");
+
+ const api = useAPI(true);
+ const v0api = useAPI(false);
const { t } = useT();
const { data: registrationsQuery, isFetching } = useQuery({
@@ -23,16 +41,30 @@ const TabCompetitors: React.FC = ({ id }) => {
}),
});
+ const { data: psychSheetQuery, isFetching: isFetchingPsychsheets } = useQuery(
+ {
+ queryKey: ["psychSheets", id, psychSheetEvent, sortBy],
+ queryFn: () =>
+ v0api.GET("/competitions/{competitionId}/psych-sheet/{eventId}", {
+ params: {
+ path: { competitionId: id, eventId: psychSheetEvent! },
+ query: { sort_by: sortBy },
+ },
+ }),
+ enabled: psychSheetEvent !== null,
+ },
+ );
+
const eventIds = useMemo(() => {
const flatEventList = registrationsQuery?.data?.flatMap(
- (reg) => reg.event_ids,
+ (reg) => reg.competing.event_ids,
);
const eventSet = new Set(flatEventList);
return Array.from(eventSet);
}, [registrationsQuery?.data]);
- if (isFetching) {
+ if (isFetching || isFetchingPsychsheets) {
return (
@@ -47,40 +79,28 @@ const TabCompetitors: React.FC = ({ id }) => {
return (
-
-
-
- Competitor
- Country
- {eventIds.map((eventId) => (
-
-
-
- ))}
-
-
-
-
- {registrationsQuery.data.map((registration) => (
-
-
- {registration.user_id}
-
-
-
-
-
- {eventIds.map((eventId) => (
-
- {registration.event_ids.includes(eventId) ? (
-
- ) : null}
-
- ))}
-
- ))}
-
-
+
+ setPsychSheetEvent(event)}
+ onClearClick={() => setPsychSheetEvent(null)}
+ />
+
+ {psychSheetEvent && (
+
+ )}
+ {!psychSheetEvent && (
+
+ )}
);
diff --git a/next-frontend/src/components/competitions/TabMenu.tsx b/next-frontend/src/components/competitions/TabMenu.tsx
index 3b11941104e..f68a74eae7d 100644
--- a/next-frontend/src/components/competitions/TabMenu.tsx
+++ b/next-frontend/src/components/competitions/TabMenu.tsx
@@ -113,7 +113,7 @@ export default function TabMenu({
{tabs.map((tab) => (
- {t(tab.i18nKey)}
+ {t(tab.i18nKey)}
))}
diff --git a/next-frontend/src/lib/wca/useAPI.ts b/next-frontend/src/lib/wca/useAPI.ts
index 38ce004e909..96f471ba02b 100644
--- a/next-frontend/src/lib/wca/useAPI.ts
+++ b/next-frontend/src/lib/wca/useAPI.ts
@@ -2,15 +2,15 @@ import { useSession } from "next-auth/react";
import { useMemo } from "react";
import { authenticatedClient, unauthenticatedClient } from "@/lib/wca/wcaAPI";
-export default function useAPI() {
+export default function useAPI(v1: boolean = false) {
const { data: session } = useSession();
return useMemo(() => {
- if (session) {
+ if (false) {
// @ts-expect-error TODO: Fix this
return authenticatedClient(session.accessToken);
} else {
- return unauthenticatedClient;
+ return unauthenticatedClient(v1);
}
- }, [session]);
+ }, [session, v1]);
}
diff --git a/next-frontend/src/lib/wca/wcaAPI.ts b/next-frontend/src/lib/wca/wcaAPI.ts
index 75a7cc3b38b..8eb68f6a273 100644
--- a/next-frontend/src/lib/wca/wcaAPI.ts
+++ b/next-frontend/src/lib/wca/wcaAPI.ts
@@ -14,10 +14,14 @@ export const serverClientWithToken = (token: string) =>
},
});
-export const unauthenticatedClient = createClient({
- baseUrl: process.env.NEXT_PUBLIC_WCA_FRONTEND_API_URL,
- headers: { "Content-Type": "application/json" },
-});
+export const unauthenticatedClient = (v1: boolean) =>
+ createClient({
+ baseUrl: process.env.NEXT_PUBLIC_WCA_FRONTEND_API_URL?.replace(
+ "v0",
+ v1 ? "v1" : "v0",
+ ),
+ headers: { "Content-Type": "application/json" },
+ });
export const authenticatedClient = (token: string) =>
createClient({
baseUrl: process.env.NEXT_PUBLIC_WCA_FRONTEND_API_URL,
diff --git a/next-frontend/src/types/openapi.ts b/next-frontend/src/types/openapi.ts
index 57ad35e9902..f7578b1188d 100644
--- a/next-frontend/src/types/openapi.ts
+++ b/next-frontend/src/types/openapi.ts
@@ -156,6 +156,47 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/competitions/{competitionId}/psych-sheet/{eventId}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get competition registrations */
+ get: {
+ parameters: {
+ query?: {
+ sort_by?: string;
+ };
+ header?: never;
+ path: {
+ competitionId: string;
+ eventId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["PsychSheet"];
+ };
+ };
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/competitions/{competitionId}/podiums": {
parameters: {
query?: never;
@@ -1019,11 +1060,50 @@ export interface components {
id?: string;
name?: string;
};
+ PsychSheet: {
+ sort_by: string;
+ sort_by_second: string;
+ sorted_rankings: {
+ name: string;
+ user_id: number;
+ wca_id: string;
+ country_iso2: string;
+ average_best: number;
+ average_rank: number;
+ single_best: number;
+ single_rank: number;
+ tied_previous: boolean;
+ pos: number;
+ }[];
+ };
RegistrationData: {
id: number;
- competition_id: string;
+ registrant_id: number;
user_id: number;
- event_ids: string[];
+ guests?: number;
+ user: {
+ id: number;
+ name: string;
+ gender: string;
+ country_iso2: string;
+ wca_id?: string;
+ };
+ competing: {
+ event_ids: string[];
+ registration_status?: string;
+ /** Format: datetime */
+ registered_on?: string;
+ comment?: string;
+ admin_comment?: string;
+ };
+ payment?: {
+ has_paid?: boolean;
+ payment_status?: string;
+ paid_amount_iso?: number;
+ currency_code?: string;
+ /** Format: datetime */
+ updated_at?: string;
+ };
};
WcifEvent: {
/** @example 333 */