diff --git a/src/atoms/atoms.ts b/src/atoms/atoms.ts index 2e6a79b4..62361703 100644 --- a/src/atoms/atoms.ts +++ b/src/atoms/atoms.ts @@ -7,9 +7,11 @@ import { type EngineSettings, engineSchema, } from "@/utils/engines"; -import type { - LichessGamesOptions, - MasterGamesOptions, +import { + type LichessGamesOptions, + type MasterGamesOptions, + lichessGamesOptionsSchema, + masterOptionsSchema, } from "@/utils/lichess/explorer"; import type { MissingMove } from "@/utils/repertoire"; import { type Tab, genID, tabSchema } from "@/utils/tabs"; @@ -266,11 +268,19 @@ export const lichessOptionsAtom = atomWithStorage( speeds: ["bullet", "blitz", "rapid", "classical", "correspondence"], color: "white", }, + createZodStorage(lichessGamesOptionsSchema, localStorage), + { + getOnInit: true, + }, ); export const masterOptionsAtom = atomWithStorage( "lichess-master-options", {}, + createZodStorage(masterOptionsSchema, localStorage), + { + getOnInit: true, + }, ); const dbTypeFamily = atomFamily((tab: string) => diff --git a/src/components/panels/database/options/LichessOptionsPanel.tsx b/src/components/panels/database/options/LichessOptionsPanel.tsx index f4440ab7..647a74b9 100644 --- a/src/components/panels/database/options/LichessOptionsPanel.tsx +++ b/src/components/panels/database/options/LichessOptionsPanel.tsx @@ -2,10 +2,10 @@ import { lichessOptionsAtom } from "@/atoms/atoms"; import ToggleButtonGroup, { type ToggleButtonGroupOption, } from "@/components/common/ToggleButtonGroup"; +import { MIN_DATE } from "@/utils/lichess/api"; import type { LichessGameSpeed, LichessRating } from "@/utils/lichess/explorer"; import { Group, Select, Stack, TextInput } from "@mantine/core"; import { MonthPickerInput } from "@mantine/dates"; -import { useDebouncedValue } from "@mantine/hooks"; import { IconChevronRight, IconChevronsRight, @@ -15,21 +15,10 @@ import { IconSend, } from "@tabler/icons-react"; import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; import { match } from "ts-pattern"; const LichessOptionsPanel = () => { - const [originalOptions, setOriginalOptions] = useAtom(lichessOptionsAtom); - const [options, setOptions] = useState(originalOptions); - const [debouncedOptions] = useDebouncedValue(options, 500); - - useEffect(() => { - setOptions(originalOptions); - }, [originalOptions]); - - useEffect(() => { - setOriginalOptions(debouncedOptions); - }, [debouncedOptions, setOriginalOptions]); + const [options, setOptions] = useAtom(lichessOptionsAtom); const timeControls: LichessGameSpeed[] = [ "ultraBullet", @@ -122,7 +111,9 @@ const LichessOptionsPanel = () => { setOptions({ ...options, since: value ?? undefined }) } @@ -131,7 +122,9 @@ const LichessOptionsPanel = () => { setOptions({ ...options, until: value ?? undefined }) } diff --git a/src/components/panels/database/options/MastersOptionsPanel.tsx b/src/components/panels/database/options/MastersOptionsPanel.tsx index ea7aa0fa..b2ba8706 100644 --- a/src/components/panels/database/options/MastersOptionsPanel.tsx +++ b/src/components/panels/database/options/MastersOptionsPanel.tsx @@ -1,4 +1,5 @@ import { masterOptionsAtom } from "@/atoms/atoms"; +import { MIN_DATE } from "@/utils/lichess/api"; import { Group } from "@mantine/core"; import { YearPickerInput } from "@mantine/dates"; import { useAtom } from "jotai"; @@ -10,7 +11,9 @@ const MasterOptionsPanel = () => { setOptions({ ...options, since: value ?? undefined }) } @@ -19,7 +22,9 @@ const MasterOptionsPanel = () => { setOptions({ ...options, until: value ?? undefined }) } diff --git a/src/utils/lichess/api.tsx b/src/utils/lichess/api.tsx index cd0e4a71..5a1641b5 100644 --- a/src/utils/lichess/api.tsx +++ b/src/utils/lichess/api.tsx @@ -30,6 +30,8 @@ const baseURL = "https://lichess.org/api"; const explorerURL = "https://explorer.lichess.ovh"; const tablebaseURL = "https://tablebase.lichess.ovh"; +export const MIN_DATE = new Date(1952, 0, 1); + export type TablebaseCategory = | "win" | "unknown" @@ -344,6 +346,10 @@ export async function getLichessGames( () => `${explorerURL}/player?${getLichessGamesQueryParams(fen, options)}`, ); const res = await fetch(url); + + if (!res.ok) { + throw new Error(`${res.data}`); + } return res.data; } @@ -355,7 +361,11 @@ export async function getMasterGames( fen, options, )}`; - return (await fetch(url)).data; + const res = await fetch(url); + if (!res.ok) { + throw new Error(`${res.data}`); + } + return res.data; } export async function getPlayerGames( diff --git a/src/utils/lichess/explorer.ts b/src/utils/lichess/explorer.ts index 0cefc5a8..debfde7c 100644 --- a/src/utils/lichess/explorer.ts +++ b/src/utils/lichess/explorer.ts @@ -1,24 +1,78 @@ -export type LichessGamesOptions = { +import { z } from "zod"; + +const lichessVariant = z.enum([ + "standard", + "chess960", + "crazyhouse", + "antichess", + "atomic", + "horde", + "kingOfTheHill", + "racingKings", + "threeCheck", + "fromPosition", +]); + +const lichessGameSpeed = z.enum([ + "ultraBullet", + "bullet", + "blitz", + "rapid", + "classical", + "correspondence", +]); +export type LichessGameSpeed = z.infer; + +export type LichessRating = + | 0 + | 1000 + | 1200 + | 1400 + | 1600 + | 1800 + | 2000 + | 2200 + | 2500; + +export const lichessGamesOptionsSchema = z.object({ //https://lichess.org/api#tag/Opening-Explorer/operation/openingExplorerLichess - variant?: LichessVariant; - speeds?: LichessGameSpeed[]; - ratings?: LichessRating[]; - since?: Date; - until?: Date; - moves?: number; - topGames?: number; - recentGames?: number; - player?: string; - color: "white" | "black"; -}; + variant: lichessVariant.optional(), + speeds: z.array(lichessGameSpeed).optional(), + ratings: z + .array( + z.union([ + z.literal(0), + z.literal(1000), + z.literal(1200), + z.literal(1400), + z.literal(1600), + z.literal(1800), + z.literal(2000), + z.literal(2200), + z.literal(2500), + ]), + ) + .optional(), + since: z.coerce.date().optional(), + until: z.coerce.date().optional(), + moves: z.number().min(0).optional(), + topGames: z.number().min(0).optional(), + recentGames: z.number().min(0).optional(), + player: z.string().optional(), + color: z.enum(["white", "black"]), +}); -export type MasterGamesOptions = { +export type LichessGamesOptions = z.infer; + +export const masterOptionsSchema = z.object({ //https://lichess.org/api#tag/Opening-Explorer/operation/openingExplorerMaster - since?: Date; - until?: Date; - moves?: number; - topGames?: number; -}; + since: z.coerce.date().optional(), + until: z.coerce.date().optional(), + moves: z.number().min(0).optional(), + topGames: z.number().min(0).optional(), +}); + +export type MasterGamesOptions = z.infer; export function getLichessGamesQueryParams( fen: string, @@ -86,34 +140,3 @@ export function getMasterGamesQueryParams( } return queryParams.join("&"); } - -export type LichessVariant = - | "standard" - | "chess960" - | "crazyhouse" - | "antichess" - | "atomic" - | "horde" - | "kingOfTheHill" - | "racingKings" - | "threeCheck" - | "fromPosition"; - -export type LichessGameSpeed = - | "ultraBullet" - | "bullet" - | "blitz" - | "rapid" - | "classical" - | "correspondence"; - -export type LichessRating = - | 0 - | 1000 - | 1200 - | 1400 - | 1600 - | 1800 - | 2000 - | 2200 - | 2500;