mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-22 20:16: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";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { last } from "lodash";
|
||||
import { isEmpty, isNil, last } from "lodash";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||
import { IoAdd, IoCloseOutline } from "react-icons/io5";
|
||||
@@ -11,12 +11,13 @@ import { z } from "zod";
|
||||
import { RadioGroupField } from "@/components/form/RadioGroupField";
|
||||
import { SelectField } from "@/components/form/SelectField";
|
||||
import { TextField } from "@/components/form/TextField";
|
||||
import useFormPersist from "@/hooks/useFormPersist";
|
||||
import { getNewRating } from "@/utils/eloCalculator";
|
||||
|
||||
import { KFactor } from "./KFactor";
|
||||
|
||||
const resultsSchema = z.object({
|
||||
currentElo: z.number().int().positive(),
|
||||
currentElo: z.number().int().positive().nullish(),
|
||||
kFactor: z.enum(["40", "30", "20", "15", "10"]),
|
||||
games: z.array(
|
||||
z.object({
|
||||
@@ -40,6 +41,7 @@ export const ManualEloForm = () => {
|
||||
const form = useForm<EloFormValues>({
|
||||
resolver: zodResolver(resultsSchema),
|
||||
defaultValues: {
|
||||
currentElo: null,
|
||||
kFactor: "20",
|
||||
games: [{}],
|
||||
},
|
||||
@@ -54,6 +56,12 @@ export const ManualEloForm = () => {
|
||||
name: "games",
|
||||
});
|
||||
|
||||
useFormPersist("manualElo", {
|
||||
watch: form.watch,
|
||||
setValue: form.setValue,
|
||||
storage: window.localStorage,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: EloFormValues) => {};
|
||||
|
||||
const [currentElo, kFactor, games] = form.watch([
|
||||
@@ -62,6 +70,13 @@ export const ManualEloForm = () => {
|
||||
"games",
|
||||
]);
|
||||
|
||||
const isDefault =
|
||||
isEmpty(currentElo) &&
|
||||
kFactor === "20" &&
|
||||
games?.length === 1 &&
|
||||
(isEmpty(games[0]) ||
|
||||
(isEmpty(games[0]?.opponentElo) && isEmpty(games[0]?.result)));
|
||||
|
||||
type Deltas = {
|
||||
rating: number;
|
||||
deltas: { rating: number; delta: number | undefined }[];
|
||||
@@ -70,7 +85,11 @@ export const ManualEloForm = () => {
|
||||
const calculations = !Number.isNaN(currentElo)
|
||||
? (games ?? []).reduce<Deltas>(
|
||||
(acc, game) => {
|
||||
if (!Number.isNaN(game?.opponentElo) && game?.result) {
|
||||
if (
|
||||
!isNil(game?.opponentElo) &&
|
||||
!Number.isNaN(game?.opponentElo) &&
|
||||
game?.result
|
||||
) {
|
||||
const result =
|
||||
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">
|
||||
{gameFields.map((game, i) => {
|
||||
return (
|
||||
<div key={i} className="flex w-full flex-col">
|
||||
<div key={i} className="flex w-full items-center space-x-2">
|
||||
<div key={game.id} className="flex w-full flex-col">
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<TextField
|
||||
name={`games.${i}.opponentElo`}
|
||||
@@ -223,7 +242,18 @@ export const ManualEloForm = () => {
|
||||
</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
|
||||
onClick={() => {
|
||||
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...",
|
||||
"gameResult": "{result, select, win {Win} draw {Draw} loss {Loss} other {{result}}}",
|
||||
"addGameButton": "Add another game result",
|
||||
"clearButton": "Clear",
|
||||
|
||||
"searchTournamentPlaceholder": "Find a tournament...",
|
||||
"noTournamentsFound": "No tournaments found",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"gameResultPlaceholder": "Résultat...",
|
||||
"gameResult": "{result, select, win {Victoire} draw {Nul} loss {Défaite} other {{result}}}",
|
||||
"addGameButton": "Ajouter un autre résultat de partie",
|
||||
"clearButton": "Effacer",
|
||||
|
||||
"searchTournamentPlaceholder": "Rechecher un tournois...",
|
||||
"noTournamentsFound": "Aucun tournoi trouvé",
|
||||
|
||||
Reference in New Issue
Block a user