mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 12:36:57 +00:00
Persist manual ELO form to local storage
Persist manual ELO form to local storage
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { last } from "lodash";
|
import { isEmpty, isNil, last } from "lodash";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { IoAdd, IoCloseOutline } from "react-icons/io5";
|
import { IoAdd, IoCloseOutline } from "react-icons/io5";
|
||||||
@@ -11,12 +11,13 @@ import { z } from "zod";
|
|||||||
import { RadioGroupField } from "@/components/form/RadioGroupField";
|
import { RadioGroupField } from "@/components/form/RadioGroupField";
|
||||||
import { SelectField } from "@/components/form/SelectField";
|
import { SelectField } from "@/components/form/SelectField";
|
||||||
import { TextField } from "@/components/form/TextField";
|
import { TextField } from "@/components/form/TextField";
|
||||||
|
import useFormPersist from "@/hooks/useFormPersist";
|
||||||
import { getNewRating } from "@/utils/eloCalculator";
|
import { getNewRating } from "@/utils/eloCalculator";
|
||||||
|
|
||||||
import { KFactor } from "./KFactor";
|
import { KFactor } from "./KFactor";
|
||||||
|
|
||||||
const resultsSchema = z.object({
|
const resultsSchema = z.object({
|
||||||
currentElo: z.number().int().positive(),
|
currentElo: z.number().int().positive().nullish(),
|
||||||
kFactor: z.enum(["40", "30", "20", "15", "10"]),
|
kFactor: z.enum(["40", "30", "20", "15", "10"]),
|
||||||
games: z.array(
|
games: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -40,6 +41,7 @@ export const ManualEloForm = () => {
|
|||||||
const form = useForm<EloFormValues>({
|
const form = useForm<EloFormValues>({
|
||||||
resolver: zodResolver(resultsSchema),
|
resolver: zodResolver(resultsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
currentElo: null,
|
||||||
kFactor: "20",
|
kFactor: "20",
|
||||||
games: [{}],
|
games: [{}],
|
||||||
},
|
},
|
||||||
@@ -54,6 +56,12 @@ export const ManualEloForm = () => {
|
|||||||
name: "games",
|
name: "games",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useFormPersist("manualElo", {
|
||||||
|
watch: form.watch,
|
||||||
|
setValue: form.setValue,
|
||||||
|
storage: window.localStorage,
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: EloFormValues) => {};
|
const onSubmit = async (data: EloFormValues) => {};
|
||||||
|
|
||||||
const [currentElo, kFactor, games] = form.watch([
|
const [currentElo, kFactor, games] = form.watch([
|
||||||
@@ -62,6 +70,13 @@ export const ManualEloForm = () => {
|
|||||||
"games",
|
"games",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const isDefault =
|
||||||
|
isEmpty(currentElo) &&
|
||||||
|
kFactor === "20" &&
|
||||||
|
games?.length === 1 &&
|
||||||
|
(isEmpty(games[0]) ||
|
||||||
|
(isEmpty(games[0]?.opponentElo) && isEmpty(games[0]?.result)));
|
||||||
|
|
||||||
type Deltas = {
|
type Deltas = {
|
||||||
rating: number;
|
rating: number;
|
||||||
deltas: { rating: number; delta: number | undefined }[];
|
deltas: { rating: number; delta: number | undefined }[];
|
||||||
@@ -70,7 +85,11 @@ export const ManualEloForm = () => {
|
|||||||
const calculations = !Number.isNaN(currentElo)
|
const calculations = !Number.isNaN(currentElo)
|
||||||
? (games ?? []).reduce<Deltas>(
|
? (games ?? []).reduce<Deltas>(
|
||||||
(acc, game) => {
|
(acc, game) => {
|
||||||
if (!Number.isNaN(game?.opponentElo) && game?.result) {
|
if (
|
||||||
|
!isNil(game?.opponentElo) &&
|
||||||
|
!Number.isNaN(game?.opponentElo) &&
|
||||||
|
game?.result
|
||||||
|
) {
|
||||||
const result =
|
const result =
|
||||||
game?.result === "win" ? 1 : game?.result === "loss" ? 0 : 0.5;
|
game?.result === "win" ? 1 : game?.result === "loss" ? 0 : 0.5;
|
||||||
|
|
||||||
@@ -136,8 +155,8 @@ export const ManualEloForm = () => {
|
|||||||
<div className="flex w-full flex-col gap-6 sm:gap-2">
|
<div className="flex w-full flex-col gap-6 sm:gap-2">
|
||||||
{gameFields.map((game, i) => {
|
{gameFields.map((game, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i} className="flex w-full flex-col">
|
<div key={game.id} className="flex w-full flex-col">
|
||||||
<div key={i} className="flex w-full items-center space-x-2">
|
<div className="flex w-full items-center space-x-2">
|
||||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<TextField
|
<TextField
|
||||||
name={`games.${i}.opponentElo`}
|
name={`games.${i}.opponentElo`}
|
||||||
@@ -223,7 +242,18 @@ export const ManualEloForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex justify-end">
|
<div className="mt-8 flex justify-end gap-4">
|
||||||
|
{!isDefault && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="px-5 py-3 text-center text-sm font-medium text-primary-600 hover:text-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:text-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
||||||
|
>
|
||||||
|
{t("clearButton")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appendGame({});
|
appendGame({});
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { SetFieldValue } from "react-hook-form";
|
||||||
|
|
||||||
|
export interface FormPersistConfig {
|
||||||
|
storage?: Storage;
|
||||||
|
watch: (names?: string | string[]) => any;
|
||||||
|
setValue: SetFieldValue<any>;
|
||||||
|
exclude?: string[];
|
||||||
|
onDataRestored?: (data: any) => void;
|
||||||
|
validate?: boolean;
|
||||||
|
dirty?: boolean;
|
||||||
|
touch?: boolean;
|
||||||
|
onTimeout?: () => void;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormPersist = (
|
||||||
|
name: string,
|
||||||
|
{
|
||||||
|
storage,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
exclude = [],
|
||||||
|
onDataRestored,
|
||||||
|
validate = false,
|
||||||
|
dirty = false,
|
||||||
|
touch = false,
|
||||||
|
onTimeout,
|
||||||
|
timeout,
|
||||||
|
}: FormPersistConfig,
|
||||||
|
) => {
|
||||||
|
const watchedValues = watch();
|
||||||
|
|
||||||
|
const getStorage = () => storage || window.sessionStorage;
|
||||||
|
|
||||||
|
const clearStorage = () => getStorage().removeItem(name);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const str = getStorage().getItem(name);
|
||||||
|
|
||||||
|
if (str) {
|
||||||
|
const { _timestamp = null, ...values } = JSON.parse(str);
|
||||||
|
const dataRestored: { [key: string]: any } = {};
|
||||||
|
const currTimestamp = Date.now();
|
||||||
|
|
||||||
|
if (timeout && currTimestamp - _timestamp > timeout) {
|
||||||
|
onTimeout && onTimeout();
|
||||||
|
clearStorage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(values).forEach((key) => {
|
||||||
|
const shouldSet = !exclude.includes(key);
|
||||||
|
if (shouldSet) {
|
||||||
|
dataRestored[key] = values[key];
|
||||||
|
setValue(key, values[key], {
|
||||||
|
shouldValidate: validate,
|
||||||
|
shouldDirty: dirty,
|
||||||
|
shouldTouch: touch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onDataRestored) {
|
||||||
|
onDataRestored(dataRestored);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => getStorage().setItem(name, JSON.stringify(dataRestored));
|
||||||
|
}
|
||||||
|
}, [storage, name, onDataRestored, setValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const values = exclude.length
|
||||||
|
? Object.entries(watchedValues)
|
||||||
|
.filter(([key]) => !exclude.includes(key))
|
||||||
|
.reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {})
|
||||||
|
: Object.assign({}, watchedValues);
|
||||||
|
|
||||||
|
if (Object.entries(values).length) {
|
||||||
|
if (timeout !== undefined) {
|
||||||
|
values._timestamp = Date.now();
|
||||||
|
}
|
||||||
|
getStorage().setItem(name, JSON.stringify(values));
|
||||||
|
}
|
||||||
|
}, [watchedValues, timeout]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
clear: () => getStorage().removeItem(name),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFormPersist;
|
||||||
@@ -151,6 +151,7 @@
|
|||||||
"gameResultPlaceholder": "Result...",
|
"gameResultPlaceholder": "Result...",
|
||||||
"gameResult": "{result, select, win {Win} draw {Draw} loss {Loss} other {{result}}}",
|
"gameResult": "{result, select, win {Win} draw {Draw} loss {Loss} other {{result}}}",
|
||||||
"addGameButton": "Add another game result",
|
"addGameButton": "Add another game result",
|
||||||
|
"clearButton": "Clear",
|
||||||
|
|
||||||
"searchTournamentPlaceholder": "Find a tournament...",
|
"searchTournamentPlaceholder": "Find a tournament...",
|
||||||
"noTournamentsFound": "No tournaments found",
|
"noTournamentsFound": "No tournaments found",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"gameResultPlaceholder": "Résultat...",
|
"gameResultPlaceholder": "Résultat...",
|
||||||
"gameResult": "{result, select, win {Victoire} draw {Nul} loss {Défaite} other {{result}}}",
|
"gameResult": "{result, select, win {Victoire} draw {Nul} loss {Défaite} other {{result}}}",
|
||||||
"addGameButton": "Ajouter un autre résultat de partie",
|
"addGameButton": "Ajouter un autre résultat de partie",
|
||||||
|
"clearButton": "Effacer",
|
||||||
|
|
||||||
"searchTournamentPlaceholder": "Rechecher un tournois...",
|
"searchTournamentPlaceholder": "Rechecher un tournois...",
|
||||||
"noTournamentsFound": "Aucun tournoi trouvé",
|
"noTournamentsFound": "Aucun tournoi trouvé",
|
||||||
|
|||||||
Reference in New Issue
Block a user