Persist manual ELO form to local storage

Persist manual ELO form to local storage
This commit is contained in:
Timothy Armes
2023-11-28 17:44:14 +01:00
committed by Owen Rees
parent 8caabe810a
commit a2289f6181
4 changed files with 131 additions and 6 deletions
+36 -6
View File
@@ -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({});
+93
View File
@@ -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;
+1
View File
@@ -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",
+1
View File
@@ -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é",