Elo calculator - display cadence in tournament dropdown + link to FFE results

This commit is contained in:
Timothy Armes
2024-04-15 14:51:23 +02:00
parent 0540ecc05a
commit c0f22d9712
3 changed files with 70 additions and 33 deletions
+7 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -15,6 +15,8 @@ import { SelectField } from "@/components/form/SelectField";
import { TournamentSelectField } from "@/components/form/TournamentSelectField"; import { TournamentSelectField } from "@/components/form/TournamentSelectField";
import { fetchTournamentResultsSchema } from "@/schemas"; import { fetchTournamentResultsSchema } from "@/schemas";
import { fetchTournamentResults } from "@/server/fetchTournamentResults"; import { fetchTournamentResults } from "@/server/fetchTournamentResults";
import { SearchedTournament } from "@/server/searchTournaments";
import { TimeControl } from "@/types";
import { Link } from "@/utils/navigation"; import { Link } from "@/utils/navigation";
import { KFactor } from "./KFactor"; import { KFactor } from "./KFactor";
@@ -42,6 +44,7 @@ export default function Elo() {
() => new URLSearchParams(Array.from(searchParams.entries())), () => new URLSearchParams(Array.from(searchParams.entries())),
[searchParams], [searchParams],
); );
const [tournament, setTournament] = useState<SearchedTournament | null>(null);
const tournamentId = searchParams.get("tId") ?? ""; const tournamentId = searchParams.get("tId") ?? "";
const kFactorParam = searchParams.get("k"); const kFactorParam = searchParams.get("k");
@@ -205,6 +208,9 @@ export default function Elo() {
control={form.control} control={form.control}
placeholder={t("searchTournamentPlaceholder")} placeholder={t("searchTournamentPlaceholder")}
noOptionsMessage={() => t("noTournamentsFound")} noOptionsMessage={() => t("noTournamentsFound")}
onInformChange={(tournaments) =>
setTournament(tournaments?.[0]?.data ?? null)
}
/> />
{isFetching && ( {isFetching && (
+13 -2
View File
@@ -32,6 +32,7 @@ export type AsyncSelectFieldProps<
required?: boolean; required?: boolean;
loadOption: (id: T) => Promise<BaseOption<T, D> | undefined>; loadOption: (id: T) => Promise<BaseOption<T, D> | undefined>;
loadOptions: (searchString?: string) => Promise<BaseOption<T, D>[]>; loadOptions: (searchString?: string) => Promise<BaseOption<T, D>[]>;
onInformChange?: (data: BaseOption<T, D>[] | null) => void;
separators?: boolean; separators?: boolean;
} }
>; >;
@@ -52,9 +53,11 @@ export const AsyncSelectField = <
loadOption, loadOption,
loadOptions, loadOptions,
onInformChange,
placeholder, placeholder,
separators, separators,
...selectProps ...selectProps
}: AsyncSelectFieldProps<TFieldValues, TFieldName, IsMulti, T, D>) => { }: AsyncSelectFieldProps<TFieldValues, TFieldName, IsMulti, T, D>) => {
const t = useTranslations("App"); const t = useTranslations("App");
@@ -92,6 +95,12 @@ export const AsyncSelectField = <
setLoadingValues(false); setLoadingValues(false);
onInformChange?.(
values.map(
(v) => valueOptions.current.find((o) => o.value === v)!,
) as BaseOption<T, D>[],
);
// This forced a state update, which enures that the select is updated once the query has finished // This forced a state update, which enures that the select is updated once the query has finished
setLoadedInitialValues(true); setLoadedInitialValues(true);
} }
@@ -102,7 +111,7 @@ export const AsyncSelectField = <
}; };
doLoad(); doLoad();
}, [value, setLoadingValues, loadOption]); }, [value, setLoadingValues, loadOption, onInformChange]);
const { const {
formState: { errors }, formState: { errors },
@@ -133,15 +142,17 @@ export const AsyncSelectField = <
newValue: OnChangeValue<BaseOption<T, D>, IsMulti>, newValue: OnChangeValue<BaseOption<T, D>, IsMulti>,
actionMeta: ActionMeta<BaseOption<T, D>>, actionMeta: ActionMeta<BaseOption<T, D>>,
) => { ) => {
console.log(newValue, actionMeta);
if (isNil(newValue) || actionMeta.action === "clear") { if (isNil(newValue) || actionMeta.action === "clear") {
onChange(null); onChange(null);
onInformChange?.(null);
valueOptions.current = []; valueOptions.current = [];
} else if (isArray(newValue)) { } else if (isArray(newValue)) {
onChange(newValue.map((option) => option.value)); onChange(newValue.map((option) => option.value));
onInformChange?.(newValue);
valueOptions.current = newValue; valueOptions.current = newValue;
} else { } else {
onChange((newValue as BaseOption<T, D>).value); onChange((newValue as BaseOption<T, D>).value);
onInformChange?.([newValue as BaseOption<T, D>]);
valueOptions.current = [newValue as BaseOption<T, D>]; valueOptions.current = [newValue as BaseOption<T, D>];
} }
}; };
@@ -1,5 +1,8 @@
import { useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { FieldPath, FieldValues } from "react-hook-form"; import { FieldPath, FieldValues } from "react-hook-form";
import { FaExternalLinkAlt } from "react-icons/fa";
import { TimeControlColours } from "@/constants"; import { TimeControlColours } from "@/constants";
import { getTournamentDetails } from "@/server/getTournamentDetails"; import { getTournamentDetails } from "@/server/getTournamentDetails";
@@ -31,6 +34,8 @@ export const TournamentSelectField = <
...rest ...rest
}: TournamentSelectFieldProps<TFieldValues, TFieldName>) => { }: TournamentSelectFieldProps<TFieldValues, TFieldName>) => {
const at = useTranslations("App"); const at = useTranslations("App");
const [selectedTournament, setSelectedTournament] =
useState<SearchedTournament | null>(null);
const loadOption = async (ffeId: string) => { const loadOption = async (ffeId: string) => {
const { data: tournament } = await getTournamentDetails({ ffeId }); const { data: tournament } = await getTournamentDetails({ ffeId });
@@ -57,6 +62,7 @@ export const TournamentSelectField = <
}; };
return ( return (
<div className="flex flex-col gap-2">
<AsyncSelectField <AsyncSelectField
{...rest} {...rest}
isSearchable isSearchable
@@ -64,6 +70,9 @@ export const TournamentSelectField = <
loadOption={loadOption} loadOption={loadOption}
loadOptions={loadOptions} loadOptions={loadOptions}
isClearable={true} isClearable={true}
onInformChange={(data) =>
setSelectedTournament(data ? data[0].data! : null)
}
formatOptionLabel={(option, context) => { formatOptionLabel={(option, context) => {
if (context.context === "value") return option.label; if (context.context === "value") return option.label;
return ( return (
@@ -86,5 +95,16 @@ export const TournamentSelectField = <
); );
}} }}
/> />
{selectedTournament && (
<a
href={selectedTournament.url}
target="_blank"
className="inline-flex items-center justify-end gap-1 text-xs text-primary hover:text-primary-800"
>
{selectedTournament.url}
<FaExternalLinkAlt />
</a>
)}
</div>
); );
}; };