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 */