diff --git a/src/app/[locale]/elo/page.tsx b/src/app/[locale]/elo/page.tsx index 1e1c23f..0002092 100644 --- a/src/app/[locale]/elo/page.tsx +++ b/src/app/[locale]/elo/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { isEmpty, sortBy } from "lodash"; @@ -11,7 +11,7 @@ import { z } from "zod"; import { Spinner } from "@/components/Spinner"; import { SelectField } from "@/components/form/SelectField"; -import { TextField } from "@/components/form/TextField"; +import { TournamentSelectField } from "@/components/form/TournamentSelectField"; import { fetchTournamentResultsSchema } from "@/schemas"; import { Link } from "@/utils/navigation"; import { trpc } from "@/utils/trpc"; @@ -23,6 +23,7 @@ import { TournamentResults } from "./TournamentResults"; const kFactors = ["40", "20", "10"]; const formSchema = fetchTournamentResultsSchema.extend({ + tournamentId: z.string().optional(), player: z.string().optional(), kFactor: z.string(), }); @@ -41,14 +42,14 @@ export default function Elo() { [searchParams], ); - const url = searchParams.get("url") ?? ""; + const tournamentId = searchParams.get("tId") ?? ""; const kFactorParam = searchParams.get("k"); const player = searchParams.get("player") ?? ""; const kFactor = kFactorParam && kFactors.includes(kFactorParam) ? kFactorParam : "20"; - const hasUrl = !isEmpty(url.trim()); + const hasTournamentId = !isEmpty(tournamentId.trim()); const { data: allResults, @@ -56,13 +57,13 @@ export default function Elo() { error, } = trpc.fetchTournamentResults.useQuery( { - url: url?.trim(), + id: tournamentId?.trim(), }, { refetchOnWindowFocus: false, staleTime: Infinity, cacheTime: 10 * 60 * 1000, - enabled: hasUrl, + enabled: hasTournamentId, retry: false, }, ); @@ -78,29 +79,18 @@ export default function Elo() { const form = useForm({ resolver: zodResolver(fetchTournamentResultsSchema), defaultValues: { - url: url ?? "", + tournamentId: tournamentId ?? "", kFactor: kFactor ?? "20", player: player ?? "", }, }); - const onSubmit = async (input: FetchResultsFormValues) => { - current.set("url", input.url); - if (input.kFactor) current.set("k", input.kFactor); - - const search = current.toString(); - const query = search ? `?${search}` : ""; - - // Push the new URL - router.push(`${pathname}${query}`); - }; - - const clearForm = () => { - current.delete("url"); + const clearForm = useCallback(() => { + current.delete("tId"); current.delete("k"); current.delete("player"); - form.setValue("url", ""); + form.setValue("tournamentId", ""); form.setValue("kFactor", "20"); form.setValue("player", ""); @@ -108,14 +98,31 @@ export default function Elo() { const query = search ? `?${search}` : ""; router.push(`${pathname}${query}`); - }; + }, [current, form, pathname, router]); useEffect(() => { // We subscribe to form changes and update the URL parameters const subscription = form.watch((value, { name, type }) => { let update = false; if (type === "change") { - if (name === "player" && value.player !== player && value.player) { + if ( + name === "tournamentId" && + value.tournamentId !== tournamentId && + value.tournamentId + ) { + current.set("tId", value.tournamentId); + update = true; + } else if ( + name === "tournamentId" && + value.tournamentId !== tournamentId && + value.tournamentId === null + ) { + clearForm(); + } else if ( + name === "player" && + value.player !== player && + value.player + ) { current.set("player", value.player); update = true; } else if ( @@ -135,12 +142,22 @@ export default function Elo() { } }); return () => subscription.unsubscribe(); - }, [current, form, form.watch, kFactor, pathname, player, router]); + }, [ + clearForm, + current, + form, + form.watch, + kFactor, + pathname, + player, + router, + tournamentId, + ]); useEffect(() => { // When the URL changes, we update the form values - if (url !== form.getValues("url")) { - form.setValue("url", url); + if (tournamentId !== form.getValues("tournamentId")) { + form.setValue("tournamentId", tournamentId); } if (player !== form.getValues("player")) { @@ -150,7 +167,7 @@ export default function Elo() { if (kFactor !== form.getValues("kFactor")) { form.setValue("kFactor", form.getValues("kFactor")); } - }, [searchParams, form, url, player, kFactor]); + }, [searchParams, form, player, kFactor, tournamentId]); if (error) { console.error(error); @@ -171,7 +188,7 @@ export default function Elo() { {t("info")}

- {hasUrl && !isFetching && !error && ( + {hasTournamentId && !isFetching && !error && (
-
+ + t("noTournamentsFound")} + /> {isFetching && (
@@ -253,15 +261,19 @@ export default function Elo() { - {hasUrl && !isFetching && player && playerResults && !error && ( - - )} + {hasTournamentId && + !isFetching && + player && + playerResults && + !error && ( + + )} - {((!hasUrl && !isFetching) || !!error) && ( + {((!hasTournamentId && !isFetching) || !!error) && ( <>
diff --git a/src/app/[locale]/tournaments/page.tsx b/src/app/[locale]/tournaments/page.tsx index 72a4704..95c78f4 100644 --- a/src/app/[locale]/tournaments/page.tsx +++ b/src/app/[locale]/tournaments/page.tsx @@ -3,19 +3,13 @@ import { groupBy } from "lodash"; import clientPromise from "@/lib/mongodb"; import { TournamentData } from "@/types"; -import { TimeControl, Tournament } from "@/types"; +import { TimeControl, Tournament, tcMap } from "@/types"; import { errorLog } from "@/utils/logger"; import TournamentsDisplay from "./TournamentsDisplay"; export const revalidate = 3600; // Revalidate cache every 6 hours -const tcMap: Record = { - "Cadence Lente": TimeControl.Classic, - Rapide: TimeControl.Rapid, - Blitz: TimeControl.Blitz, -}; - const getTournaments = async () => { try { const client = await clientPromise; @@ -35,7 +29,7 @@ const getTournaments = async () => { }, { $match: { - status: { $ne: "completed" } + status: { $ne: "completed" }, }, }, { @@ -94,6 +88,7 @@ const getTournaments = async () => { return { id: t._id.toString(), + ffeId: t.tournament_id, groupId, tournament: t.tournament, town: t.town, diff --git a/src/components/form/AsyncSelectField.tsx b/src/components/form/AsyncSelectField.tsx new file mode 100644 index 0000000..bd3217f --- /dev/null +++ b/src/components/form/AsyncSelectField.tsx @@ -0,0 +1,187 @@ +import React, { useEffect, useRef, useState } from "react"; + +import { get, isArray, isNil } from "lodash"; +import { useTranslations } from "next-intl"; +import { + Controller, + FieldPath, + FieldValues, + useFormContext, + useWatch, +} from "react-hook-form"; +import { ActionMeta, GroupBase, OnChangeValue, Props } from "react-select"; +import AsyncSelect from "react-select/async"; + +import { Prettify } from "@/types"; + +import { Field, GenericFieldProps } from "./Field"; +import { BaseOption, classNames } from "./SelectField"; + +export type AsyncSelectFieldProps< + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, + IsMulti extends boolean = false, + T = string, + D = unknown, +> = Prettify< + GenericFieldProps & + Omit< + Props, IsMulti, GroupBase>>, + "options" | "onChange" | "value" | "classNames" + > & { + required?: boolean; + loadOption: (id: T) => Promise | undefined>; + loadOptions: (searchString?: string) => Promise[]>; + separators?: boolean; + } +>; + +export const AsyncSelectField = < + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, + IsMulti extends boolean = false, + T = string, + D = unknown, +>({ + name, + control, + className, + label, + hideErrorMessage, + required, + + loadOption, + loadOptions, + + placeholder, + separators, + ...selectProps +}: AsyncSelectFieldProps) => { + const t = useTranslations("App"); + const [loadingValues, setLoadingValues] = useState(false); + const [loadedInitialValues, setLoadedInitialValues] = useState(false); + const [loadingOptions, setLoadingOptions] = useState(false); + const valueOptions = useRef[]>([]); + const searchOptions = useRef[]>([]); + const form = useFormContext(); + const value = useWatch({ control: form.control, name }); + + // Fetch the option(s) for the current value(s) when mounted + useEffect(() => { + const doLoad = async () => { + try { + const values = !value ? [] : isArray(value) ? value : [value]; + + const unFetchedValues = values.filter( + (v) => + valueOptions.current.find((o) => o.value === value) === undefined, + ); + + if (unFetchedValues.length > 0) { + setLoadingValues(true); + const options = await Promise.all( + unFetchedValues.map((v: T) => { + return loadOption(v); + }), + ); + + valueOptions.current = [ + ...valueOptions.current, + ...options.filter((vo): vo is BaseOption => vo !== undefined), + ]; + + setLoadingValues(false); + + // This forced a state update, which enures that the select is updated once the query has finished + setLoadedInitialValues(true); + } + } catch (e) { + console.log(e); + setLoadingValues(false); + } + }; + + doLoad(); + }, [value, setLoadingValues, loadOption]); + + const { + formState: { errors }, + } = form; + + const hasError = !!get(errors, name)?.message; + + const valueToOption = (value: T) => + [...valueOptions.current, ...searchOptions.current].find( + (o) => o.value === value, + ) ?? { value, label: "" }; + + const fetchOptions = async (searchString?: string) => { + setLoadingOptions(true); + const options = await loadOptions(searchString); + searchOptions.current = options; + setLoadingOptions(false); + return options; + }; + + return ( + + { + const onSelectChange = ( + newValue: OnChangeValue, IsMulti>, + actionMeta: ActionMeta>, + ) => { + console.log(newValue, actionMeta); + if (isNil(newValue) || actionMeta.action === "clear") { + onChange(null); + valueOptions.current = []; + } else if (isArray(newValue)) { + onChange(newValue.map((option) => option.value)); + valueOptions.current = newValue; + } else { + onChange((newValue as BaseOption).value); + valueOptions.current = [newValue as BaseOption]; + } + }; + + const optionValue = + isNil(value) || value === "" + ? null + : isArray(value) + ? // @ts-ignore - this is too complex for TS to understand + value.map(valueToOption) + : valueToOption(value); + + return ( + , IsMulti> + isClearable + value={optionValue} + onChange={onSelectChange} + loadOptions={fetchOptions} + defaultOptions + isLoading={loadingOptions || loadingValues} + noOptionsMessage={() => t("noOptionsMessage")} + placeholder={placeholder ?? t("selectPlaceholder")} + unstyled + styles={{ + input: (base) => ({ + ...base, + "input:focus": { + boxShadow: "none", + }, + }), + }} + classNames={classNames, IsMulti>( + hasError, + separators ?? false, + )} + {...selectProps} + /> + ); + }} + /> + + ); +}; diff --git a/src/components/form/SelectField.tsx b/src/components/form/SelectField.tsx index d29bd66..109b0d1 100644 --- a/src/components/form/SelectField.tsx +++ b/src/components/form/SelectField.tsx @@ -29,12 +29,12 @@ export type BaseOption = { export const classNames = ( hasError: boolean, + separators: boolean, ): ClassNamesConfig> => ({ container: () => "w-full", valueContainer: () => "text-gray-900 dark:text-white", indicatorsContainer: () => "flex items-center self-stretch shrink-0", - clearIndicator: () => - "pointer-events-none flex items-center pr-2 text-gray-900 dark:text-white", + clearIndicator: () => "flex items-center pr-2 text-gray-900 dark:text-white", dropdownIndicator: () => "pointer-events-none flex items-center pr-2 text-gray-900 dark:text-white", indicatorSeparator: () => "w-px text-gray-900 dark:text-white", @@ -55,7 +55,7 @@ export const classNames = ( "block truncate pr-2 placeholder-gray dark:placeholder-gray-400", menu: () => twMerge( - "!z-10 mt-2 rounded-lg border border-gray-200 bg-white p-1", + "!z-30 mt-2 rounded-lg border border-gray-200 bg-white p-1", "border-gray-300 bg-gray-50 text-gray-900", "dark:border-gray-600 dark:bg-gray-700 dark:text-white", ), @@ -63,6 +63,7 @@ export const classNames = ( option: ({ isFocused, isDisabled }) => twMerge( "px-3 py-2 hover:cursor-pointer", + separators && "border-b border-gray-200", isDisabled && "opacity-50", isFocused && "hover:bg-primary-500 hover:text-white", ), @@ -83,6 +84,7 @@ export type SelectFieldProps< "onChange" | "value" | "classNames" | "name" > & { required?: boolean; + separators?: boolean; } >; @@ -103,6 +105,7 @@ export const SelectField = < required, placeholder, + separators, options, ...selectProps }: SelectFieldProps) => { @@ -176,7 +179,10 @@ export const SelectField = < }, }), }} - classNames={classNames, IsMulti>(hasError)} + classNames={classNames, IsMulti>( + hasError, + separators ?? false, + )} {...selectProps} /> ); diff --git a/src/components/form/TournamentSelectField.tsx b/src/components/form/TournamentSelectField.tsx new file mode 100644 index 0000000..ed63d3e --- /dev/null +++ b/src/components/form/TournamentSelectField.tsx @@ -0,0 +1,87 @@ +import { useTranslations } from "next-intl"; +import { FieldPath, FieldValues } from "react-hook-form"; + +import { TimeControlColours } from "@/constants"; +import { SearchedTournament, trpc } from "@/utils/trpc"; + +import { AsyncSelectField, AsyncSelectFieldProps } from "./AsyncSelectField"; + +type TournamentSelectFieldProps< + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, +> = Omit< + AsyncSelectFieldProps< + TFieldValues, + TFieldName, + false, + string, + SearchedTournament + >, + "loadOptions" | "loadOption" | "separators" +>; + +export const TournamentSelectField = < + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, +>({ + ...rest +}: TournamentSelectFieldProps) => { + const at = useTranslations("App"); + const client = trpc.useContext(); + + const loadOption = async (ffeId: string) => { + const tournament = await client.getTournamentDetails.fetch({ ffeId }); + + return !tournament + ? undefined + : { + value: tournament.ffeId, + label: tournament.tournament, + data: tournament, + }; + }; + + const loadOptions = async (searchValue?: string) => { + const tournaments = await client.searchTournaments.fetch({ + searchValue: searchValue ?? "", + }); + + return (tournaments ?? []).map((tournament) => ({ + value: tournament.ffeId, + label: tournament.tournament, + data: tournament, + })); + }; + + return ( + { + if (context.context === "value") return option.label; + return ( +
+
+
{option.data?.town}
+
+ {at("timeControlEnum", { tc: option.data?.timeControl })} +
+
{option.data?.date}
+
+ +
{option.data?.tournament}
+
+ ); + }} + /> + ); +}; diff --git a/src/messages/en.json b/src/messages/en.json index 5724460..2794ea8 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -152,9 +152,8 @@ "gameResult": "{result, select, win {Win} draw {Draw} loss {Loss} other {{result}}}", "addGameButton": "Add another game result", - "resultsUrlLabel": "Paste a link to the FFE results page", - "resultsUrlPlaceholder": "http://echecs.asso.fr/FicheTournoi.aspx?Ref=...", - "fetchResultsButton": "Fetch", + "searchTournamentPlaceholder": "Find a tournament...", + "noTournamentsFound": "No tournaments found", "enterManualResultsButton": "Enter results manually", "choosePlayerLabel": "Choose a player", "choosePlayerPlaceholder": "Player", diff --git a/src/messages/fr.json b/src/messages/fr.json index 96799bc..d4dbf22 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -154,9 +154,8 @@ "gameResult": "{result, select, win {Victoire} draw {Nul} loss {Défaite} other {{result}}}", "addGameButton": "Ajouter un autre résultat de partie", - "resultsUrlLabel": "Collez un lien vers la page de résultats de la FFE", - "resultsUrlPlaceholder": "http://echecs.asso.fr/FicheTournoi.aspx?Ref=...", - "fetchResultsButton": "Récupérer", + "searchTournamentPlaceholder": "Rechecher un tournois...", + "noTournamentsFound": "Aucun tournoi trouvé", "enterManualResultsButton": "Saisir les résultats manuellement", "choosePlayerLabel": "Choisissez un joueur", "choosePlayerPlaceholder": "Joueur", diff --git a/src/schemas.ts b/src/schemas.ts index 6ed7fee..5a78cc0 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -28,5 +28,5 @@ export const addTournamentSchema = z.object({ }); export const fetchTournamentResultsSchema = z.object({ - url: z.string().url({ message: "FormValidation.url" }), + id: z.string().min(1, { message: "FormValidation.required" }), }); diff --git a/src/server/appRouter.ts b/src/server/appRouter.ts index 2bbb061..1fb6f92 100644 --- a/src/server/appRouter.ts +++ b/src/server/appRouter.ts @@ -1,11 +1,15 @@ import { addTournament } from "./procedures/addTournament"; import { contactUs } from "./procedures/contactUs"; import { fetchTournamentResults } from "./procedures/fetchTournamentResults"; +import { getTournamentDetails } from "./procedures/getTournamentDetails"; +import { searchTournaments } from "./procedures/searchTournaments"; import { router } from "./trpc"; export const appRouter = router({ contactUs, addTournament, + searchTournaments, + getTournamentDetails, fetchTournamentResults, }); diff --git a/src/server/procedures/addTournament.ts b/src/server/procedures/addTournament.ts index f039045..70ec5d5 100644 --- a/src/server/procedures/addTournament.ts +++ b/src/server/procedures/addTournament.ts @@ -24,7 +24,7 @@ export const addTournament = publicProcedure const { name, email, message, tournament } = input; - const tournamentData: Omit = { + const tournamentData: Omit = { ...tournament, date: format(tournament.date, "dd/MM/yyyy"), time_control: tcMap[tournament.time_control], diff --git a/src/server/procedures/fetchTournamentResults.ts b/src/server/procedures/fetchTournamentResults.ts index 410329a..ff9f9b5 100644 --- a/src/server/procedures/fetchTournamentResults.ts +++ b/src/server/procedures/fetchTournamentResults.ts @@ -5,7 +5,7 @@ import { errorLog } from "@/utils/logger"; import { publicProcedure } from "../trpc"; -const scrapedSchema = z.array( +const dbSchema = z.array( z.object({ id: z.string(), name: z.string(), @@ -55,7 +55,7 @@ const getEloDetails = (elo: string | null) => { }; const getResultDetails = ( - results: z.infer[number]["results"][number], + results: z.infer[number]["results"][number], ) => { if (results.opponent_name === null) { return { result: 0, lostByForfeit: true, wonByForfeit: false }; @@ -107,17 +107,6 @@ export const fetchTournamentResults = publicProcedure .output(outputSchema) .query(async ({ input }) => { try { - // Depending on which page the user copied the URL from, we need to extract the tournament ID - - // http://echecs.asso.fr/FicheTournoi.aspx?Ref=59975 - // http://echecs.asso.fr/Resultats.aspx?URL=Tournois/Id/59975/59975&Action=Ls - - const tournamentId = - input.url.match(/Ref=(\d+)/)?.[1] ?? input.url.match(/Id\/(\d+)/)?.[1]; - if (!tournamentId) { - throw new Error("ERR_NO_TOURNAMENT_ID"); - } - const headers = new Headers(); const apiKey = process.env.RESULTS_API_KEY; @@ -125,8 +114,10 @@ export const fetchTournamentResults = publicProcedure headers.append("api-key", apiKey); } + console.log(input.id); + const rawResults = await fetch( - `${process.env.RESULTS_SCRAPER_URL}${tournamentId}`, + `${process.env.RESULTS_SCRAPER_URL}${input.id}`, { headers: headers, }, @@ -138,7 +129,7 @@ export const fetchTournamentResults = publicProcedure throw new Error("ERR_TOURNAMENT_NOT_FOUND"); } - const parsedResults = scrapedSchema.parse(results); + const parsedResults = dbSchema.parse(results); return parsedResults.map[number]>( (player) => ({ id: player.id, @@ -158,7 +149,7 @@ export const fetchTournamentResults = publicProcedure }), ); } catch (error) { - reportFetchError(input.url, error); + reportFetchError(input.id, error); errorLog(JSON.stringify(error, null, 2)); throw error; } diff --git a/src/server/procedures/getTournamentDetails.ts b/src/server/procedures/getTournamentDetails.ts new file mode 100644 index 0000000..1e36352 --- /dev/null +++ b/src/server/procedures/getTournamentDetails.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; + +import clientPromise from "@/lib/mongodb"; +import { TournamentData } from "@/types"; +import { TimeControl, tcMap } from "@/types"; +import { errorLog } from "@/utils/logger"; + +import { publicProcedure } from "../trpc"; + +const inputSchema = z.object({ + ffeId: z.string(), +}); + +export const getTournamentDetails = publicProcedure + .input(inputSchema) + .query(async ({ input }) => { + try { + const client = await clientPromise; + const db = client.db("tournamentsFranceDB"); + const t = await db + .collection("tournaments") + .findOne({ tournament_id: input.ffeId }); + + if (t === null) { + return null; + } + + const timeControl = tcMap[t.time_control] ?? TimeControl.Other; + + return { + id: t._id.toString(), + ffeId: t.tournament_id, + tournament: t.tournament, + town: t.town, + department: t.department, + date: t.date, + url: t.url, + timeControl, + status: t.status, + }; + } catch (error) { + errorLog(JSON.stringify(error, null, 2)); + throw error; + } + }); diff --git a/src/server/procedures/searchTournaments.ts b/src/server/procedures/searchTournaments.ts new file mode 100644 index 0000000..adea96b --- /dev/null +++ b/src/server/procedures/searchTournaments.ts @@ -0,0 +1,96 @@ +import { endOfDay } from "date-fns"; +import { z } from "zod"; + +import clientPromise from "@/lib/mongodb"; +import { TournamentData } from "@/types"; +import { TimeControl, Tournament, tcMap } from "@/types"; +import { errorLog } from "@/utils/logger"; +import { removeDiacritics } from "@/utils/string"; + +import { publicProcedure } from "../trpc"; + +const inputSchema = z.object({ + searchValue: z.string(), + limit: z.number().optional(), +}); + +type SearchedTournament = Pick< + Tournament, + | "id" + | "ffeId" + | "date" + | "department" + | "status" + | "timeControl" + | "tournament" + | "town" + | "url" +>; + +export const searchTournaments = publicProcedure + .input(inputSchema) + .query(async ({ input }) => { + try { + const client = await clientPromise; + const db = client.db("tournamentsFranceDB"); + + const searchTerms = input.searchValue + .split(" ") + .map((s) => removeDiacritics(s.trim())) + .filter((s) => s !== "") + .map((s) => ({ tournament_index: { $regex: s, $options: "i" } })); + + const data = await db + .collection("tournaments") + .aggregate([ + { + $addFields: { + dateParts: { + $dateFromString: { + dateString: "$end_date", + format: "%d/%m/%Y", + }, + }, + }, + }, + { + $match: { + $and: [ + { federation: { $eq: "FFE" } }, + { dateParts: { $lte: endOfDay(new Date()) } }, + ...searchTerms, + ], + }, + }, + { + $sort: { + dateParts: -1, + }, + }, + { $limit: input.limit ?? 20 }, + { + $unset: "dateParts", + }, + ]) + .toArray(); + + return data.map((t) => { + const timeControl = tcMap[t.time_control] ?? TimeControl.Other; + + return { + id: t._id.toString(), + ffeId: t.tournament_id, + tournament: t.tournament, + town: t.town, + department: t.department, + date: t.date, + url: t.url, + timeControl, + status: t.status, + }; + }); + } catch (error) { + errorLog(JSON.stringify(error, null, 2)); + throw error; + } + }); diff --git a/src/types.ts b/src/types.ts index 69d65f6..9f9aaac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export type Status = "scheduled" | "ongoing" | "finished"; export type TournamentData = { _id: ObjectId; + tournament_id: string; town: string; department: string; country: string; @@ -38,6 +39,7 @@ export enum TimeControl { export type Tournament = { id: string; + ffeId: string; groupId: string; town: string; department: string; @@ -51,6 +53,12 @@ export type Tournament = { status: Status; }; +export const tcMap: Record = { + "Cadence Lente": TimeControl.Classic, + Rapide: TimeControl.Rapid, + Blitz: TimeControl.Blitz, +}; + export type Club = { id: string; name: string; diff --git a/src/utils/trpc.ts b/src/utils/trpc.ts index 1f838dc..506206d 100644 --- a/src/utils/trpc.ts +++ b/src/utils/trpc.ts @@ -7,5 +7,6 @@ export type APIRouterInput = inferRouterInputs; export type APIRouterOutput = inferRouterOutputs; export type TournamentResultsData = APIRouterOutput["fetchTournamentResults"]; +export type SearchedTournament = APIRouterOutput["searchTournaments"][number]; export const trpc = createTRPCReact();