From 46198f2a47a27487e8a2c0e1a5adaca6aa59276f Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Sun, 17 Mar 2024 18:57:22 +0100 Subject: [PATCH] feat : add lichess user games import --- src/lib/chessCom.ts | 2 +- src/lib/lichess.ts | 20 +++++ src/sections/loadGame/chessComInput.tsx | 4 +- src/sections/loadGame/lichessInput.tsx | 101 +++++++++++++++++++++++ src/sections/loadGame/loadGameDialog.tsx | 6 ++ src/types/enums.ts | 1 + src/types/lichess.ts | 16 ++++ 7 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 src/sections/loadGame/lichessInput.tsx diff --git a/src/lib/chessCom.ts b/src/lib/chessCom.ts index e0b578f..867cff2 100644 --- a/src/lib/chessCom.ts +++ b/src/lib/chessCom.ts @@ -1,7 +1,7 @@ import { ChessComGame } from "@/types/chessCom"; import { getPaddedMonth } from "./helpers"; -export const getUserRecentGames = async ( +export const getChessComUserRecentGames = async ( username: string ): Promise => { const date = new Date(); diff --git a/src/lib/lichess.ts b/src/lib/lichess.ts index bb32569..a1b710a 100644 --- a/src/lib/lichess.ts +++ b/src/lib/lichess.ts @@ -3,6 +3,7 @@ import { sortLines } from "./engine/helpers/parseResults"; import { LichessError, LichessEvalBody, + LichessGame, LichessResponse, } from "@/types/lichess"; @@ -54,3 +55,22 @@ export const getLichessEval = async ( }; } }; + +export const getLichessUserRecentGames = async ( + username: string +): Promise => { + const res = await fetch( + `https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc`, + { method: "GET", headers: { accept: "application/x-ndjson" } } + ); + + if (res.status === 404) return []; + + const rawData = await res.text(); + const games: LichessGame[] = rawData + .split("\n") + .filter((game) => game.length > 0) + .map((game) => JSON.parse(game)); + + return games; +}; diff --git a/src/sections/loadGame/chessComInput.tsx b/src/sections/loadGame/chessComInput.tsx index 7d30a19..e263d21 100644 --- a/src/sections/loadGame/chessComInput.tsx +++ b/src/sections/loadGame/chessComInput.tsx @@ -1,5 +1,5 @@ import { useLocalStorage } from "@/hooks/useLocalStorage"; -import { getUserRecentGames } from "@/lib/chessCom"; +import { getChessComUserRecentGames } from "@/lib/chessCom"; import { capitalize } from "@/lib/helpers"; import { ChessComGame } from "@/types/chessCom"; import { @@ -33,7 +33,7 @@ export default function ChessComInput({ pgn, setPgn }: Props) { const timeout = setTimeout( async () => { - const games = await getUserRecentGames(chessComUsername); + const games = await getChessComUserRecentGames(chessComUsername); setGames(games); }, requestCount === 0 ? 0 : 500 diff --git a/src/sections/loadGame/lichessInput.tsx b/src/sections/loadGame/lichessInput.tsx new file mode 100644 index 0000000..eb466de --- /dev/null +++ b/src/sections/loadGame/lichessInput.tsx @@ -0,0 +1,101 @@ +import { useLocalStorage } from "@/hooks/useLocalStorage"; +import { getLichessUserRecentGames } from "@/lib/lichess"; +import { capitalize } from "@/lib/helpers"; +import { LichessGame } from "@/types/lichess"; +import { + CircularProgress, + FormControl, + Grid, + ListItemButton, + ListItemText, + TextField, +} from "@mui/material"; +import { useEffect, useState } from "react"; + +interface Props { + pgn: string; + setPgn: (pgn: string) => void; +} + +export default function LichessInput({ pgn, setPgn }: Props) { + const [requestCount, setRequestCount] = useState(0); + const [lichessUsername, setLichessUsername] = useLocalStorage( + "lichess-username", + "" + ); + 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 + + return ( + <> + + setLichessUsername(e.target.value)} + /> + + + {lichessUsername && ( + + {games.map((game) => ( + setPgn(game.pgn)} + selected={pgn === game.pgn} + style={{ width: 350, maxWidth: 350 }} + key={game.id} + > + + + ))} + + {games.length === 0 && } + + )} + + ); +} diff --git a/src/sections/loadGame/loadGameDialog.tsx b/src/sections/loadGame/loadGameDialog.tsx index 8c417e4..e98aaf3 100644 --- a/src/sections/loadGame/loadGameDialog.tsx +++ b/src/sections/loadGame/loadGameDialog.tsx @@ -20,6 +20,7 @@ import { useState } from "react"; import GamePgnInput from "./gamePgnInput"; import ChessComInput from "./chessComInput"; import { useLocalStorage } from "@/hooks/useLocalStorage"; +import LichessInput from "./lichessInput"; interface Props { open: boolean; @@ -105,6 +106,10 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { )} + {gameOrigin === GameOrigin.Lichess && ( + + )} + {parsingError && ( @@ -133,4 +138,5 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { const gameOriginLabel: Record = { [GameOrigin.Pgn]: "PGN", [GameOrigin.ChessCom]: "Chess.com", + [GameOrigin.Lichess]: "Lichess.org", }; diff --git a/src/types/enums.ts b/src/types/enums.ts index d658e2d..2bc2a99 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -1,6 +1,7 @@ export enum GameOrigin { Pgn = "pgn", ChessCom = "chesscom", + Lichess = "lichess", } export enum EngineName { diff --git a/src/types/lichess.ts b/src/types/lichess.ts index b151448..dc91d34 100644 --- a/src/types/lichess.ts +++ b/src/types/lichess.ts @@ -16,3 +16,19 @@ export type LichessResponse = T | LichessErrorBody; export enum LichessError { NotFound = "Not found", } + +export interface LichessGame { + id: string; + speed: string; + lastMoveAt: number; + players: { + white: LichessGameUser; + black: LichessGameUser; + }; + pgn: string; +} + +export interface LichessGameUser { + user?: { id: string; name: string }; + rating?: number; +}