From 34fdde282cadc508425baacaa8b100201b30fa14 Mon Sep 17 00:00:00 2001 From: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Fri, 9 May 2025 01:53:34 +0200 Subject: [PATCH] feat : improve game loading flow --- src/hooks/useDebounce.ts | 24 +++++ src/sections/loadGame/chessComInput.tsx | 108 +++++++++++----------- src/sections/loadGame/lichessInput.tsx | 112 +++++++++++------------ src/sections/loadGame/loadGameDialog.tsx | 7 +- 4 files changed, 137 insertions(+), 114 deletions(-) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..dbc7bfe --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; + +export function useDebounce(value: T, delayMs: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + if (value === debouncedValue) return; + + if (!debouncedValue) { + setDebouncedValue(value); + return; + } + + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delayMs); + + return () => { + clearTimeout(handler); + }; + }, [value, delayMs, debouncedValue]); + + return debouncedValue; +} diff --git a/src/sections/loadGame/chessComInput.tsx b/src/sections/loadGame/chessComInput.tsx index 6fe465a..ef37e4f 100644 --- a/src/sections/loadGame/chessComInput.tsx +++ b/src/sections/loadGame/chessComInput.tsx @@ -1,7 +1,6 @@ import { useLocalStorage } from "@/hooks/useLocalStorage"; import { getChessComUserRecentGames } from "@/lib/chessCom"; import { capitalize } from "@/lib/helpers"; -import { ChessComGame } from "@/types/chessCom"; import { CircularProgress, FormControl, @@ -11,42 +10,31 @@ import { TextField, } from "@mui/material"; import { useSetAtom } from "jotai"; -import { useEffect, useState } from "react"; import { boardOrientationAtom } from "../analysis/states"; +import { useDebounce } from "@/hooks/useDebounce"; +import { useQuery } from "@tanstack/react-query"; interface Props { onSelect: (pgn: string) => void; } export default function ChessComInput({ onSelect }: Props) { - const [requestCount, setRequestCount] = useState(0); const [chessComUsername, setChessComUsername] = useLocalStorage( "chesscom-username", "" ); + const debouncedUsername = useDebounce(chessComUsername, 200); const setBoardOrientation = useSetAtom(boardOrientationAtom); - const [games, setGames] = useState([]); - useEffect(() => { - if (!chessComUsername) { - setGames([]); - return; - } - - const timeout = setTimeout( - async () => { - const games = await getChessComUserRecentGames(chessComUsername); - setGames(games); - }, - requestCount === 0 ? 0 : 500 - ); - - setRequestCount((prev) => prev + 1); - - return () => { - clearTimeout(timeout); - }; - }, [chessComUsername]); // eslint-disable-line react-hooks/exhaustive-deps + const { + data: games, + isFetching, + isError, + } = useQuery({ + queryKey: ["CCUserGames", debouncedUsername], + enabled: !!debouncedUsername, + queryFn: () => getChessComUserRecentGames(debouncedUsername ?? ""), + }); return ( <> @@ -68,38 +56,48 @@ export default function ChessComInput({ onSelect }: Props) { minHeight={100} size={12} > - {games.map((game) => ( - { - setBoardOrientation( - chessComUsername.toLowerCase() !== - game.black.username.toLowerCase() - ); - onSelect(game.pgn); - }} - style={{ width: 350, maxWidth: 350 }} - key={game.uuid} - > - + ) : isError ? ( + + User not found. Please check your username. + + ) : !games?.length ? ( + + No games found. Please check your username. + + ) : ( + games.map((game) => ( + { + setBoardOrientation( + chessComUsername.toLowerCase() !== + game.black.username.toLowerCase() + ); + onSelect(game.pgn); }} - /> - - ))} - - {games.length === 0 && } + style={{ width: 350, maxWidth: 350 }} + key={game.uuid} + > + + + )) + )} )} diff --git a/src/sections/loadGame/lichessInput.tsx b/src/sections/loadGame/lichessInput.tsx index 079e374..d7869fd 100644 --- a/src/sections/loadGame/lichessInput.tsx +++ b/src/sections/loadGame/lichessInput.tsx @@ -1,7 +1,6 @@ import { useLocalStorage } from "@/hooks/useLocalStorage"; import { getLichessUserRecentGames } from "@/lib/lichess"; import { capitalize } from "@/lib/helpers"; -import { LichessGame } from "@/types/lichess"; import { CircularProgress, FormControl, @@ -10,43 +9,32 @@ import { ListItemText, TextField, } from "@mui/material"; -import { useEffect, useState } from "react"; import { useSetAtom } from "jotai"; import { boardOrientationAtom } from "../analysis/states"; +import { useDebounce } from "@/hooks/useDebounce"; +import { useQuery } from "@tanstack/react-query"; interface Props { onSelect: (pgn: string) => void; } export default function LichessInput({ onSelect }: Props) { - const [requestCount, setRequestCount] = useState(0); const [lichessUsername, setLichessUsername] = useLocalStorage( "lichess-username", "" ); + const debouncedUsername = useDebounce(lichessUsername, 200); const setBoardOrientation = useSetAtom(boardOrientationAtom); - const [games, setGames] = useState([]); - useEffect(() => { - if (!lichessUsername) { - setGames([]); - return; - } - - const timeout = setTimeout( - async () => { - const games = await getLichessUserRecentGames(lichessUsername); - setGames(games); - }, - requestCount === 0 ? 0 : 500 - ); - - setRequestCount((prev) => prev + 1); - - return () => { - clearTimeout(timeout); - }; - }, [lichessUsername]); // eslint-disable-line react-hooks/exhaustive-deps + const { + data: games, + isFetching, + isError, + } = useQuery({ + queryKey: ["LichessUserGames", debouncedUsername], + enabled: !!debouncedUsername, + queryFn: () => getLichessUserRecentGames(debouncedUsername ?? ""), + }); return ( <> @@ -68,40 +56,50 @@ export default function LichessInput({ onSelect }: Props) { minHeight={100} size={12} > - {games.map((game) => ( - { - setBoardOrientation( - lichessUsername.toLowerCase() !== - game.players.black.user?.name.toLowerCase() - ); - onSelect(game.pgn); - }} - style={{ width: 350, maxWidth: 350 }} - key={game.id} - > - + ) : isError ? ( + + User not found. Please check your username. + + ) : !games?.length ? ( + + No games found. Please check your username. + + ) : ( + games.map((game) => ( + { + setBoardOrientation( + lichessUsername.toLowerCase() !== + game.players.black.user?.name.toLowerCase() + ); + onSelect(game.pgn); }} - /> - - ))} - - {games.length === 0 && } + style={{ width: 350, maxWidth: 350 }} + key={game.id} + > + + + )) + )} )} diff --git a/src/sections/loadGame/loadGameDialog.tsx b/src/sections/loadGame/loadGameDialog.tsx index 0da19ce..e64ff42 100644 --- a/src/sections/loadGame/loadGameDialog.tsx +++ b/src/sections/loadGame/loadGameDialog.tsx @@ -104,7 +104,10 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { displayEmpty input={} value={gameOrigin ?? ""} - onChange={(e) => setGameOrigin(e.target.value as GameOrigin)} + onChange={(e) => { + setGameOrigin(e.target.value as GameOrigin); + setParsingError(""); + }} > {Object.values(GameOrigin).map((origin) => ( @@ -128,7 +131,7 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { {parsingError && ( - + {parsingError}