feat : improve game loading flow
This commit is contained in:
24
src/hooks/useDebounce.ts
Normal file
24
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useDebounce<T>(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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
import { getChessComUserRecentGames } from "@/lib/chessCom";
|
import { getChessComUserRecentGames } from "@/lib/chessCom";
|
||||||
import { capitalize } from "@/lib/helpers";
|
import { capitalize } from "@/lib/helpers";
|
||||||
import { ChessComGame } from "@/types/chessCom";
|
|
||||||
import {
|
import {
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -11,42 +10,31 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { boardOrientationAtom } from "../analysis/states";
|
import { boardOrientationAtom } from "../analysis/states";
|
||||||
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSelect: (pgn: string) => void;
|
onSelect: (pgn: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChessComInput({ onSelect }: Props) {
|
export default function ChessComInput({ onSelect }: Props) {
|
||||||
const [requestCount, setRequestCount] = useState(0);
|
|
||||||
const [chessComUsername, setChessComUsername] = useLocalStorage(
|
const [chessComUsername, setChessComUsername] = useLocalStorage(
|
||||||
"chesscom-username",
|
"chesscom-username",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
const debouncedUsername = useDebounce(chessComUsername, 200);
|
||||||
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
||||||
const [games, setGames] = useState<ChessComGame[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (!chessComUsername) {
|
data: games,
|
||||||
setGames([]);
|
isFetching,
|
||||||
return;
|
isError,
|
||||||
}
|
} = useQuery({
|
||||||
|
queryKey: ["CCUserGames", debouncedUsername],
|
||||||
const timeout = setTimeout(
|
enabled: !!debouncedUsername,
|
||||||
async () => {
|
queryFn: () => getChessComUserRecentGames(debouncedUsername ?? ""),
|
||||||
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
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -68,38 +56,48 @@ export default function ChessComInput({ onSelect }: Props) {
|
|||||||
minHeight={100}
|
minHeight={100}
|
||||||
size={12}
|
size={12}
|
||||||
>
|
>
|
||||||
{games.map((game) => (
|
{isFetching ? (
|
||||||
<ListItemButton
|
<CircularProgress />
|
||||||
onClick={() => {
|
) : isError ? (
|
||||||
setBoardOrientation(
|
<span style={{ color: "salmon" }}>
|
||||||
chessComUsername.toLowerCase() !==
|
User not found. Please check your username.
|
||||||
game.black.username.toLowerCase()
|
</span>
|
||||||
);
|
) : !games?.length ? (
|
||||||
onSelect(game.pgn);
|
<span style={{ color: "salmon" }}>
|
||||||
}}
|
No games found. Please check your username.
|
||||||
style={{ width: 350, maxWidth: 350 }}
|
</span>
|
||||||
key={game.uuid}
|
) : (
|
||||||
>
|
games.map((game) => (
|
||||||
<ListItemText
|
<ListItemButton
|
||||||
primary={`${capitalize(game.white.username) || "White"} (${
|
onClick={() => {
|
||||||
game.white.rating || "?"
|
setBoardOrientation(
|
||||||
}) vs ${capitalize(game.black.username) || "Black"} (${
|
chessComUsername.toLowerCase() !==
|
||||||
game.black.rating || "?"
|
game.black.username.toLowerCase()
|
||||||
})`}
|
);
|
||||||
secondary={`${capitalize(game.time_class)} played at ${new Date(
|
onSelect(game.pgn);
|
||||||
game.end_time * 1000
|
|
||||||
)
|
|
||||||
.toLocaleString()
|
|
||||||
.slice(0, -3)}`}
|
|
||||||
slotProps={{
|
|
||||||
primary: { noWrap: true },
|
|
||||||
secondary: { noWrap: true },
|
|
||||||
}}
|
}}
|
||||||
/>
|
style={{ width: 350, maxWidth: 350 }}
|
||||||
</ListItemButton>
|
key={game.uuid}
|
||||||
))}
|
>
|
||||||
|
<ListItemText
|
||||||
{games.length === 0 && <CircularProgress />}
|
primary={`${capitalize(game.white.username) || "White"} (${
|
||||||
|
game.white.rating || "?"
|
||||||
|
}) vs ${capitalize(game.black.username) || "Black"} (${
|
||||||
|
game.black.rating || "?"
|
||||||
|
})`}
|
||||||
|
secondary={`${capitalize(game.time_class)} played at ${new Date(
|
||||||
|
game.end_time * 1000
|
||||||
|
)
|
||||||
|
.toLocaleString()
|
||||||
|
.slice(0, -3)}`}
|
||||||
|
slotProps={{
|
||||||
|
primary: { noWrap: true },
|
||||||
|
secondary: { noWrap: true },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
import { getLichessUserRecentGames } from "@/lib/lichess";
|
import { getLichessUserRecentGames } from "@/lib/lichess";
|
||||||
import { capitalize } from "@/lib/helpers";
|
import { capitalize } from "@/lib/helpers";
|
||||||
import { LichessGame } from "@/types/lichess";
|
|
||||||
import {
|
import {
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -10,43 +9,32 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { boardOrientationAtom } from "../analysis/states";
|
import { boardOrientationAtom } from "../analysis/states";
|
||||||
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSelect: (pgn: string) => void;
|
onSelect: (pgn: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LichessInput({ onSelect }: Props) {
|
export default function LichessInput({ onSelect }: Props) {
|
||||||
const [requestCount, setRequestCount] = useState(0);
|
|
||||||
const [lichessUsername, setLichessUsername] = useLocalStorage(
|
const [lichessUsername, setLichessUsername] = useLocalStorage(
|
||||||
"lichess-username",
|
"lichess-username",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
const debouncedUsername = useDebounce(lichessUsername, 200);
|
||||||
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
const setBoardOrientation = useSetAtom(boardOrientationAtom);
|
||||||
const [games, setGames] = useState<LichessGame[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (!lichessUsername) {
|
data: games,
|
||||||
setGames([]);
|
isFetching,
|
||||||
return;
|
isError,
|
||||||
}
|
} = useQuery({
|
||||||
|
queryKey: ["LichessUserGames", debouncedUsername],
|
||||||
const timeout = setTimeout(
|
enabled: !!debouncedUsername,
|
||||||
async () => {
|
queryFn: () => getLichessUserRecentGames(debouncedUsername ?? ""),
|
||||||
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
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -68,40 +56,50 @@ export default function LichessInput({ onSelect }: Props) {
|
|||||||
minHeight={100}
|
minHeight={100}
|
||||||
size={12}
|
size={12}
|
||||||
>
|
>
|
||||||
{games.map((game) => (
|
{isFetching ? (
|
||||||
<ListItemButton
|
<CircularProgress />
|
||||||
onClick={() => {
|
) : isError ? (
|
||||||
setBoardOrientation(
|
<span style={{ color: "salmon" }}>
|
||||||
lichessUsername.toLowerCase() !==
|
User not found. Please check your username.
|
||||||
game.players.black.user?.name.toLowerCase()
|
</span>
|
||||||
);
|
) : !games?.length ? (
|
||||||
onSelect(game.pgn);
|
<span style={{ color: "salmon" }}>
|
||||||
}}
|
No games found. Please check your username.
|
||||||
style={{ width: 350, maxWidth: 350 }}
|
</span>
|
||||||
key={game.id}
|
) : (
|
||||||
>
|
games.map((game) => (
|
||||||
<ListItemText
|
<ListItemButton
|
||||||
primary={`${
|
onClick={() => {
|
||||||
capitalize(game.players.white.user?.name || "white") ||
|
setBoardOrientation(
|
||||||
"White"
|
lichessUsername.toLowerCase() !==
|
||||||
} (${game.players?.white?.rating || "?"}) vs ${
|
game.players.black.user?.name.toLowerCase()
|
||||||
capitalize(game.players.black.user?.name || "black") ||
|
);
|
||||||
"Black"
|
onSelect(game.pgn);
|
||||||
} (${game.players?.black?.rating || "?"})`}
|
|
||||||
secondary={`${capitalize(game.speed)} played at ${new Date(
|
|
||||||
game.lastMoveAt
|
|
||||||
)
|
|
||||||
.toLocaleString()
|
|
||||||
.slice(0, -3)}`}
|
|
||||||
slotProps={{
|
|
||||||
primary: { noWrap: true },
|
|
||||||
secondary: { noWrap: true },
|
|
||||||
}}
|
}}
|
||||||
/>
|
style={{ width: 350, maxWidth: 350 }}
|
||||||
</ListItemButton>
|
key={game.id}
|
||||||
))}
|
>
|
||||||
|
<ListItemText
|
||||||
{games.length === 0 && <CircularProgress />}
|
primary={`${
|
||||||
|
capitalize(game.players.white.user?.name || "white") ||
|
||||||
|
"White"
|
||||||
|
} (${game.players?.white?.rating || "?"}) vs ${
|
||||||
|
capitalize(game.players.black.user?.name || "black") ||
|
||||||
|
"Black"
|
||||||
|
} (${game.players?.black?.rating || "?"})`}
|
||||||
|
secondary={`${capitalize(game.speed)} played at ${new Date(
|
||||||
|
game.lastMoveAt
|
||||||
|
)
|
||||||
|
.toLocaleString()
|
||||||
|
.slice(0, -3)}`}
|
||||||
|
slotProps={{
|
||||||
|
primary: { noWrap: true },
|
||||||
|
secondary: { noWrap: true },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
|
|||||||
displayEmpty
|
displayEmpty
|
||||||
input={<OutlinedInput label="Game origin" />}
|
input={<OutlinedInput label="Game origin" />}
|
||||||
value={gameOrigin ?? ""}
|
value={gameOrigin ?? ""}
|
||||||
onChange={(e) => setGameOrigin(e.target.value as GameOrigin)}
|
onChange={(e) => {
|
||||||
|
setGameOrigin(e.target.value as GameOrigin);
|
||||||
|
setParsingError("");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{Object.values(GameOrigin).map((origin) => (
|
{Object.values(GameOrigin).map((origin) => (
|
||||||
<MenuItem key={origin} value={origin}>
|
<MenuItem key={origin} value={origin}>
|
||||||
@@ -128,7 +131,7 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
|
|||||||
|
|
||||||
{parsingError && (
|
{parsingError && (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<Typography color="red" textAlign="center" marginTop={1}>
|
<Typography color="salmon" textAlign="center" marginTop={1}>
|
||||||
{parsingError}
|
{parsingError}
|
||||||
</Typography>
|
</Typography>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
Reference in New Issue
Block a user