diff --git a/src/components/slider.tsx b/src/components/slider.tsx index 5d00c57..6ed50b5 100644 --- a/src/components/slider.tsx +++ b/src/components/slider.tsx @@ -1,8 +1,8 @@ import { Grid, Slider as MuiSlider, Typography } from "@mui/material"; -import { PrimitiveAtom, useAtom } from "jotai"; interface Props { - atom: PrimitiveAtom; + value: number; + setValue: (value: number) => void; min: number; max: number; label: string; @@ -14,12 +14,11 @@ export default function Slider({ min, max, label, - atom, + value, + setValue, xs, marksFilter = 1, }: Props) { - const [value, setValue] = useAtom(atom); - return ( ( + key: string, + atom: PrimitiveAtom +): [T, (value: SetStateAction) => void] { + const [keyTemp, setKeyTemp] = useState(""); + const [storedValue, setStoredValue] = useAtom(atom); + + useEffect(() => { + const item = window.localStorage.getItem(key); + if (item) { + setStoredValue(parseJSON(item)); + } + setKeyTemp(key); + }, [key, setStoredValue]); + + useEffect(() => { + if (keyTemp !== key) return; + window.localStorage.setItem(key, JSON.stringify(storedValue)); + }, [key, keyTemp, storedValue]); + + return [storedValue, setStoredValue]; +} + +function parseJSON(value: string): T { + return value === "undefined" ? undefined : JSON.parse(value); +} diff --git a/src/lib/chess.ts b/src/lib/chess.ts index d3be364..e8a50ca 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -21,7 +21,7 @@ export const formatGameToDatabase = (game: Chess): Omit => { event: headers.Event, site: headers.Site, date: headers.Date, - round: headers.Round, + round: headers.Round ?? "?", white: { name: headers.White, rating: headers.WhiteElo ? Number(headers.WhiteElo) : undefined, diff --git a/src/lib/chessCom.ts b/src/lib/chessCom.ts new file mode 100644 index 0000000..d73ece0 --- /dev/null +++ b/src/lib/chessCom.ts @@ -0,0 +1,41 @@ +import { ChessComGame } from "@/types/chessCom"; +import { getPaddedMonth } from "./helpers"; + +export const getUserRecentGames = async ( + username: string +): Promise => { + const date = new Date(); + const year = date.getUTCFullYear(); + const month = date.getUTCMonth() + 1; + const paddedMonth = getPaddedMonth(month); + + const res = await fetch( + `https://api.chess.com/pub/player/${username}/games/${year}/${paddedMonth}` + ); + + if (res.status === 404) return []; + + const data = await res.json(); + + const games: ChessComGame[] = data?.games ?? []; + + if (games.length < 20) { + const previousMonth = month === 1 ? 12 : month - 1; + const previousPaddedMonth = getPaddedMonth(previousMonth); + const yearToFetch = previousMonth === 12 ? year - 1 : year; + + const resPreviousMonth = await fetch( + `https://api.chess.com/pub/player/${username}/games/${yearToFetch}/${previousPaddedMonth}` + ); + + const dataPreviousMonth = await resPreviousMonth.json(); + + games.push(...(dataPreviousMonth?.games ?? [])); + } + + games.sort((a, b) => { + return b.end_time - a.end_time; + }); + + return games; +}; diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts new file mode 100644 index 0000000..5596efe --- /dev/null +++ b/src/lib/helpers.ts @@ -0,0 +1,7 @@ +export const getPaddedMonth = (month: number) => { + return month < 10 ? `0${month}` : month; +}; + +export const capitalize = (s: string) => { + return s.charAt(0).toUpperCase() + s.slice(1); +}; diff --git a/src/pages/database.tsx b/src/pages/database.tsx index 9b9061e..ca4a5c6 100644 --- a/src/pages/database.tsx +++ b/src/pages/database.tsx @@ -170,6 +170,16 @@ export default function GameDatabase() { hideFooter={true} autoHeight={true} localeText={gridLocaleText} + initialState={{ + sorting: { + sortModel: [ + { + field: "date", + sort: "desc", + }, + ], + }, + }} /> diff --git a/src/sections/engineSettings/arrowOptions.tsx b/src/sections/engineSettings/arrowOptions.tsx index 1eab450..01f5871 100644 --- a/src/sections/engineSettings/arrowOptions.tsx +++ b/src/sections/engineSettings/arrowOptions.tsx @@ -1,13 +1,19 @@ import { Checkbox, FormControlLabel, Grid } from "@mui/material"; -import { useAtom } from "jotai"; import { showBestMoveArrowAtom, showPlayerMoveArrowAtom, } from "../analysis/states"; +import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage"; export default function ArrowOptions() { - const [showBestMove, setShowBestMove] = useAtom(showBestMoveArrowAtom); - const [showPlayerMove, setShowPlayerMove] = useAtom(showPlayerMoveArrowAtom); + const [showBestMove, setShowBestMove] = useAtomLocalStorage( + "show-arrow-best-move", + showBestMoveArrowAtom + ); + const [showPlayerMove, setShowPlayerMove] = useAtomLocalStorage( + "show-arrow-player-move", + showPlayerMoveArrowAtom + ); return ( @@ -65,7 +75,8 @@ export default function EngineSettingsDialog({ open, onClose }: Props) { void; +} + +export default function ChessComInput({ pgn, setPgn }: Props) { + const [requestCount, setRequestCount] = useState(0); + const [chessComUsername, setChessComUsername] = useLocalStorage( + "chesscom-username", + "" + ); + const [games, setGames] = useState([]); + + useEffect(() => { + if (!chessComUsername) { + setGames([]); + return; + } + + const timeout = setTimeout( + async () => { + const games = await getUserRecentGames(chessComUsername); + setGames(games); + }, + requestCount === 0 ? 0 : 500 + ); + + setRequestCount((prev) => prev + 1); + + return () => { + clearTimeout(timeout); + }; + }, [chessComUsername]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + + setChessComUsername(e.target.value)} + /> + + + + {games.map((game) => ( + setPgn(game.pgn)} + selected={pgn === game.pgn} + style={{ width: 350, maxWidth: 350 }} + key={game.uuid} + > + + + ))} + + + ); +} diff --git a/src/sections/loadGame/gamePgnInput.tsx b/src/sections/loadGame/gamePgnInput.tsx new file mode 100644 index 0000000..04e253b --- /dev/null +++ b/src/sections/loadGame/gamePgnInput.tsx @@ -0,0 +1,20 @@ +import { FormControl, TextField } from "@mui/material"; + +interface Props { + pgn: string; + setPgn: (pgn: string) => void; +} + +export default function GamePgnInput({ pgn, setPgn }: Props) { + return ( + + setPgn(e.target.value)} + /> + + ); +} diff --git a/src/sections/loadGame/loadGameDialog.tsx b/src/sections/loadGame/loadGameDialog.tsx index c846303..8c417e4 100644 --- a/src/sections/loadGame/loadGameDialog.tsx +++ b/src/sections/loadGame/loadGameDialog.tsx @@ -8,16 +8,18 @@ import { Dialog, DialogTitle, DialogContent, - Box, FormControl, InputLabel, OutlinedInput, DialogActions, - TextField, Typography, + Grid, } from "@mui/material"; import { Chess } from "chess.js"; import { useState } from "react"; +import GamePgnInput from "./gamePgnInput"; +import ChessComInput from "./chessComInput"; +import { useLocalStorage } from "@/hooks/useLocalStorage"; interface Props { open: boolean; @@ -27,6 +29,10 @@ interface Props { export default function NewGameDialog({ open, onClose, setGame }: Props) { const [pgn, setPgn] = useState(""); + const [gameOrigin, setGameOrigin] = useLocalStorage( + "preferred-game-origin", + GameOrigin.Pgn + ); const [parsingError, setParsingError] = useState(""); const { addGame } = useGameDatabase(); @@ -63,11 +69,16 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { return ( - Add a game to your database + {setGame ? "Load a game" : "Add a game to your database"} - Only PGN input is supported at the moment - + Game origin - - setPgn(e.target.value)} - /> - + + {gameOrigin === GameOrigin.Pgn && ( + + )} + + {gameOrigin === GameOrigin.ChessCom && ( + + )} + {parsingError && ( @@ -101,7 +112,7 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { )} - +