style : enhance game loading and choosing UI (#43)

This commit is contained in:
Ali Raza Khalid
2025-06-10 03:58:31 +05:00
committed by GitHub
parent 0bc031eabb
commit 839f87a2e8
10 changed files with 758 additions and 98 deletions

View File

@@ -1,10 +1,10 @@
import { ChessComGame } from "@/types/chessCom"; import { ChessComRawGameData } from "@/types/chessCom";
import { getPaddedNumber } from "./helpers"; import { getPaddedNumber } from "./helpers";
export const getChessComUserRecentGames = async ( export const getChessComUserRecentGames = async (
username: string, username: string,
signal?: AbortSignal signal?: AbortSignal
): Promise<ChessComGame[]> => { ): Promise<ChessComRawGameData[]> => {
const date = new Date(); const date = new Date();
const year = date.getUTCFullYear(); const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1; const month = date.getUTCMonth() + 1;
@@ -24,7 +24,7 @@ export const getChessComUserRecentGames = async (
throw new Error("Error fetching games from Chess.com"); throw new Error("Error fetching games from Chess.com");
} }
const games: ChessComGame[] = data?.games ?? []; const games: ChessComRawGameData[] = data?.games ?? [];
if (games.length < 50) { if (games.length < 50) {
const previousMonth = month === 1 ? 12 : month - 1; const previousMonth = month === 1 ? 12 : month - 1;

View File

@@ -3,7 +3,7 @@ import { sortLines } from "./engine/helpers/parseResults";
import { import {
LichessError, LichessError,
LichessEvalBody, LichessEvalBody,
LichessGame, LichessRawGameData,
LichessResponse, LichessResponse,
} from "@/types/lichess"; } from "@/types/lichess";
import { logErrorToSentry } from "./sentry"; import { logErrorToSentry } from "./sentry";
@@ -58,7 +58,7 @@ export const getLichessEval = async (
export const getLichessUserRecentGames = async ( export const getLichessUserRecentGames = async (
username: string, username: string,
signal?: AbortSignal signal?: AbortSignal
): Promise<LichessGame[]> => { ): Promise<LichessRawGameData[]> => {
const res = await fetch( const res = await fetch(
`https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc&clocks=true`, `https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc&clocks=true`,
{ method: "GET", headers: { accept: "application/x-ndjson" }, signal } { method: "GET", headers: { accept: "application/x-ndjson" }, signal }
@@ -69,7 +69,7 @@ export const getLichessUserRecentGames = async (
} }
const rawData = await res.text(); const rawData = await res.text();
const games: LichessGame[] = rawData const games: LichessRawGameData[] = rawData
.split("\n") .split("\n")
.filter((game) => game.length > 0) .filter((game) => game.length > 0)
.map((game) => JSON.parse(game)); .map((game) => JSON.parse(game));

View File

@@ -0,0 +1,171 @@
import type React from "react";
import {
ListItem,
ListItemText,
Typography,
Box,
useTheme,
IconButton,
Tooltip,
} from "@mui/material";
import { Icon } from "@iconify/react";
import {
DateChip,
GameResult,
MovesChip,
TimeControlChip,
} from "./game-item-utils";
type ChessComPlayer = {
username: string;
rating: number;
title?: string;
};
type ChessComGameProps = {
id: string;
white: ChessComPlayer;
black: ChessComPlayer;
result: string;
timeControl: string;
date: string;
opening?: string;
moves?: number;
url: string;
onClick?: () => void;
perspectiveUserColor: "white" | "black";
};
export const ChessComGameItem: React.FC<ChessComGameProps> = ({
white,
black,
result,
timeControl,
date,
moves,
url,
onClick,
perspectiveUserColor,
}) => {
const theme = useTheme();
const formatPlayerName = (player: ChessComPlayer) => {
return player.title
? `${player.title} ${player.username}`
: player.username;
};
const whiteWon = result === "1-0";
const blackWon = result === "0-1";
return (
<ListItem
alignItems="flex-start"
sx={{
borderRadius: 2,
mb: 1.5,
transition: "all 0.2s ease-in-out",
"&:hover": {
backgroundColor: theme.palette.action.hover,
transform: "translateY(-1px)",
boxShadow: theme.shadows[3],
},
border: `1px solid ${theme.palette.divider}`,
cursor: onClick ? "pointer" : "default",
}}
onClick={onClick}
>
<ListItemText
primary={
<Box
sx={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: 1.5,
mb: 1,
}}
>
<Typography
variant="subtitle1"
component="span"
sx={{
fontWeight: "700",
color: whiteWon
? theme.palette.success.main
: theme.palette.text.primary,
opacity: blackWon ? 0.7 : 1,
}}
>
{formatPlayerName(white)} ({white.rating})
</Typography>
<Typography
variant="body2"
component="span"
sx={{ color: theme.palette.text.secondary, fontWeight: "500" }}
>
vs
</Typography>
<Typography
variant="subtitle1"
component="span"
sx={{
fontWeight: "700",
color: blackWon
? theme.palette.success.main
: theme.palette.text.primary,
opacity: whiteWon ? 0.7 : 1,
}}
>
{formatPlayerName(black)} ({black.rating})
</Typography>
<GameResult
result={result}
perspectiveUserColor={perspectiveUserColor}
/>
</Box>
}
secondary={
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 1,
alignItems: "center",
}}
>
<TimeControlChip timeControl={timeControl} />
{moves && moves > 0 && <MovesChip moves={moves} />}
<DateChip date={date} />
</Box>
}
sx={{ mr: 2 }}
/>
<Box sx={{ display: "flex", alignItems: "center", ml: "auto" }}>
<Tooltip title="View on Chess.com">
<IconButton
onClick={(e) => {
e.stopPropagation();
window.open(url, "_blank");
}}
size="small"
sx={{
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: theme.palette.action.hover,
transform: "scale(1.1)",
},
transition: "all 0.2s ease-in-out",
}}
>
<Icon icon="material-symbols:open-in-new" />
</IconButton>
</Tooltip>
</Box>
</ListItem>
);
};

View File

@@ -1,24 +1,113 @@
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 { import {
CircularProgress, CircularProgress,
FormControl, FormControl,
Grid2 as Grid, Grid2 as Grid,
ListItemButton,
ListItemText,
TextField, TextField,
List,
Autocomplete, Autocomplete,
} from "@mui/material"; } from "@mui/material";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { useDebounce } from "@/hooks/useDebounce"; import { useDebounce } from "@/hooks/useDebounce";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { ChessComGameItem } from "./chess-com-game-item";
import { ChessComRawGameData, NormalizedGameData } from "@/types/chessCom";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
interface Props { interface Props {
onSelect: (pgn: string, boardOrientation?: boolean) => void; onSelect: (pgn: string, boardOrientation?: boolean) => void;
} }
// Helper function to normalize Chess.com data
const normalizeChessComData = (
data: ChessComRawGameData
): NormalizedGameData => {
const timeControl = data.time_control + "s" || "unknown";
// Todo Convert from seconds to minutes to time + increment seconds
// Determine result from multiple sources
let gameResult = "*"; // default to ongoing
if (data.result) {
gameResult = data.result;
} else if (data.white?.result && data.black?.result) {
if (data.white.result === "win") {
gameResult = "1-0";
} else if (data.black.result === "win") {
gameResult = "0-1";
} else if (
(data.white.result === "stalemate" &&
data.black.result === "stalemate") ||
(data.white.result === "repetition" &&
data.black.result === "repetition") ||
(data.white.result === "insufficient" &&
data.black.result === "insufficient") ||
(data.white.result === "50move" && data.black.result === "50move") ||
(data.white.result === "agreed" && data.black.result === "agreed")
) {
gameResult = "1/2-1/2";
}
}
//* Function to count moves from PGN. Generated from claude..... :)
const countMovesFromPGN = (pgn: string) => {
if (!pgn) return 0;
// Split PGN into lines and find the moves section (after headers)
const lines = pgn.split("\n");
let movesSection = "";
let inMoves = false;
for (const line of lines) {
if (line.trim() === "" && !inMoves) {
inMoves = true;
continue;
}
if (inMoves) {
movesSection += line + " ";
}
}
// Remove comments in curly braces and square brackets
movesSection = movesSection
.replace(/\{[^}]*\}/g, "")
.replace(/\[[^\]]*\]/g, "");
// Remove result indicators
movesSection = movesSection.replace(/1-0|0-1|1\/2-1\/2|\*/g, "");
// Split by move numbers and count them
// Match pattern like "1." "58." etc.
const moveNumbers = movesSection.match(/\d+\./g);
return moveNumbers ? moveNumbers.length : 0;
};
return {
id: data.uuid || data.url?.split("/").pop() || data.id,
white: {
username: data.white?.username || "White",
rating: data.white?.rating || 0,
title: data.white?.title,
},
black: {
username: data.black?.username || "Black",
rating: data.black?.rating || 0,
title: data.black?.title,
},
result: gameResult,
timeControl: timeControl,
date: data.end_time
? new Date(data.end_time * 1000).toLocaleDateString()
: new Date().toLocaleDateString(),
opening: data.opening?.name || data.eco,
moves: data.pgn ? countMovesFromPGN(data.pgn) : 0,
url: data.url,
};
};
export default function ChessComInput({ onSelect }: Props) { export default function ChessComInput({ onSelect }: Props) {
const [rawStoredValue, setStoredValues] = useLocalStorage<string>( const [rawStoredValue, setStoredValues] = useLocalStorage<string>(
"chesscom-username", "chesscom-username",
@@ -148,40 +237,31 @@ export default function ChessComInput({ onSelect }: Props) {
No games found. Please check your username. No games found. Please check your username.
</span> </span>
) : ( ) : (
games.map((game) => ( <List sx={{ width: "100%", maxWidth: 800 }}>
<ListItemButton {games.map((game) => {
onClick={() => { const normalizedGame = normalizeChessComData(game);
updateHistory(debouncedUsername); const perspectiveUserColor =
const boardOrientation = normalizedGame.white.username.toLowerCase() ===
chessComUsername.toLowerCase()
? "white"
: "black";
return (
<ChessComGameItem
key={game.uuid}
{...normalizedGame}
perspectiveUserColor={perspectiveUserColor}
onClick={() => {
updateHistory(debouncedUsername);
const boardOrientation =
debouncedUsername.toLowerCase() !== debouncedUsername.toLowerCase() !==
game.black?.username?.toLowerCase(); game.black?.username?.toLowerCase();
onSelect(game.pgn, boardOrientation); onSelect(game.pgn, boardOrientation);
}} }}
style={{ width: 350, maxWidth: 350 }} />
key={game.uuid} );
> })}
<ListItemText </List>
primary={`${capitalize(game.white?.username || "white")} (${
game.white?.rating || "?"
}) vs ${capitalize(game.black?.username || "black")} (${
game.black?.rating || "?"
})`}
secondary={
game.end_time
? `${capitalize(game.time_class || "game")} played at ${new Date(
game.end_time * 1000
)
.toLocaleString()
.slice(0, -3)}`
: undefined
}
slotProps={{
primary: { noWrap: true },
secondary: { noWrap: true },
}}
/>
</ListItemButton>
))
)} )}
</Grid> </Grid>
)} )}

View File

@@ -0,0 +1,105 @@
import { Icon } from "@iconify/react";
import { Chip, Tooltip, useTheme } from "@mui/material";
import React from "react";
export const GameResult: React.FC<{
result: string;
perspectiveUserColor: "white" | "black";
}> = ({ result, perspectiveUserColor }) => {
const theme = useTheme();
let color = theme.palette.text.secondary; // Neutral gray for ongoing
let bgColor = theme.palette.action.hover;
let icon = <Icon icon="material-symbols:play-circle-outline" />;
let label = "Game in Progress";
if (result === "1-0") {
// White wins
if (perspectiveUserColor === "white") {
color = theme.palette.success.main; // Success green
bgColor = `${theme.palette.success.main}1A`; // 10% opacity
icon = <Icon icon="material-symbols:emoji-events" />;
} else {
// perspectiveUserColor is black
color = theme.palette.error.main; // Confident red
bgColor = `${theme.palette.error.main}1A`; // 10% opacity
icon = <Icon icon="material-symbols:sentiment-dissatisfied" />; // A suitable icon for loss
}
label = "White Wins";
} else if (result === "0-1") {
// Black wins
if (perspectiveUserColor === "black") {
color = theme.palette.success.main; // Success green
bgColor = `${theme.palette.success.main}1A`; // 10% opacity
icon = <Icon icon="material-symbols:emoji-events" />;
} else {
// perspectiveUserColor is white
color = theme.palette.error.main; // Confident red
bgColor = `${theme.palette.error.main}1A`; // 10% opacity
icon = <Icon icon="material-symbols:sentiment-dissatisfied" />; // A suitable icon for loss
}
label = "Black Wins";
} else if (result === "1/2-1/2") {
color = theme.palette.info.main; // Balanced blue (using info for a neutral, distinct color)
bgColor = `${theme.palette.info.main}1A`; // 10% opacity
icon = <Icon icon="material-symbols:handshake" />;
label = "Draw";
}
return (
<Tooltip title={label}>
<Chip
icon={icon}
label={result}
size="small"
sx={{
color,
backgroundColor: bgColor,
fontWeight: "600",
minWidth: 65,
border: `1px solid ${color}20`,
"& .MuiChip-icon": {
color: color,
},
}}
/>
</Tooltip>
);
};
export const TimeControlChip: React.FC<{ timeControl: string }> = ({
timeControl,
}) => {
return (
<Tooltip title="Time Control">
<Chip
icon={<Icon icon="material-symbols:timer-outline" />}
label={timeControl}
size="small"
/>
</Tooltip>
);
};
export const MovesChip: React.FC<{ moves: number }> = ({ moves }) => {
return (
<Tooltip title="Number of Moves">
<Chip
icon={<Icon icon="heroicons:hashtag-20-solid" />}
label={`${Math.round(moves / 2)} moves`}
size="small"
/>
</Tooltip>
);
};
export const DateChip: React.FC<{ date: string }> = ({ date }) => {
return (
<Tooltip title="Date Played">
<Chip
icon={<Icon icon="material-symbols:calendar-today" />}
label={date}
size="small"
/>
</Tooltip>
);
};

View File

@@ -1,4 +1,6 @@
import { FormControl, TextField } from "@mui/material"; import { FormControl, TextField, Button } from "@mui/material";
import { Icon } from "@iconify/react";
import React from "react";
interface Props { interface Props {
pgn: string; pgn: string;
@@ -6,6 +8,20 @@ interface Props {
} }
export default function GamePgnInput({ pgn, setPgn }: Props) { export default function GamePgnInput({ pgn, setPgn }: Props) {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const fileContent = e.target?.result as string;
setPgn(fileContent);
};
reader.readAsText(file); // Read the file as text
}
};
return ( return (
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
@@ -14,7 +30,22 @@ export default function GamePgnInput({ pgn, setPgn }: Props) {
multiline multiline
value={pgn} value={pgn}
onChange={(e) => setPgn(e.target.value)} onChange={(e) => setPgn(e.target.value)}
rows={8}
sx={{ mb: 2 }}
/> />
<Button
variant="contained"
component="label"
startIcon={<Icon icon="material-symbols:upload" />}
>
Choose PGN File
<input
type="file"
hidden // Hide the default file input
accept=".pgn" // Only allow .pgn files
onChange={handleFileChange}
/>
</Button>
</FormControl> </FormControl>
); );
} }

View File

@@ -0,0 +1,179 @@
import type React from "react";
import {
ListItem,
ListItemText,
Typography,
Chip,
Box,
useTheme,
IconButton,
Tooltip,
} from "@mui/material";
import { Icon } from "@iconify/react";
import {
DateChip,
GameResult,
MovesChip,
TimeControlChip,
} from "./game-item-utils";
type LichessPlayer = {
username: string;
rating: number;
title?: string;
};
type LichessGameProps = {
id: string;
white: LichessPlayer;
black: LichessPlayer;
result: string;
timeControl: string;
date: string;
opening?: string;
moves?: number;
url: string;
onClick?: () => void;
perspectiveUserColor: "white" | "black";
};
export const LichessGameItem: React.FC<LichessGameProps> = ({
white,
black,
result,
timeControl,
date,
perspectiveUserColor,
moves,
url,
onClick,
}) => {
const theme = useTheme();
// If it is a titled played append the title to the start of the name
const formatPlayerName = (player: LichessPlayer) => {
return player.title
? `${player.title} ${player.username}`
: player.username;
};
const whiteWon = result === "1-0";
const blackWon = result === "0-1";
return (
<ListItem
alignItems="flex-start"
sx={{
borderRadius: 2,
mb: 1.5,
transition: "all 0.2s ease-in-out",
"&:hover": {
backgroundColor: theme.palette.action.hover,
transform: "translateY(-1px)",
boxShadow: theme.shadows[3],
},
border: `1px solid ${theme.palette.divider}`,
cursor: onClick ? "pointer" : "default",
}}
onClick={onClick}
>
<ListItemText
primary={
<Box
sx={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: 1.5,
mb: 1,
}}
>
<Typography
variant="subtitle1"
component="span"
sx={{
fontWeight: "700",
color: whiteWon
? theme.palette.success.main
: theme.palette.text.primary,
opacity: blackWon ? 0.7 : 1,
}}
>
{formatPlayerName(white)} ({white.rating})
</Typography>
<Typography
variant="body2"
component="span"
sx={{ color: theme.palette.text.secondary, fontWeight: "500" }}
>
vs
</Typography>
<Typography
variant="subtitle1"
component="span"
sx={{
fontWeight: "700",
color: blackWon
? theme.palette.success.main
: theme.palette.text.primary,
opacity: whiteWon ? 0.7 : 1,
}}
>
{formatPlayerName(black)} ({black.rating})
</Typography>
<GameResult
result={result}
perspectiveUserColor={perspectiveUserColor}
/>
</Box>
}
secondary={
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 1,
alignItems: "center",
}}
>
<TimeControlChip timeControl={timeControl} />
{moves && moves > 0 && <MovesChip moves={moves} />}
<DateChip date={date} />
<Chip
icon={<Icon icon="simple-icons:lichess" />}
label="Lichess"
size="small"
/>
</Box>
}
sx={{ mr: 2 }}
/>
<Box sx={{ display: "flex", alignItems: "center", ml: "auto" }}>
<Tooltip title="View on Lichess">
<IconButton
onClick={(e) => {
e.stopPropagation();
window.open(url, "_blank");
}}
size="small"
sx={{
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: theme.palette.action.hover,
transform: "scale(1.1)",
},
transition: "all 0.2s ease-in-out",
}}
>
<Icon icon="material-symbols:open-in-new" />
</IconButton>
</Tooltip>
</Box>
</ListItem>
);
};

View File

@@ -1,24 +1,56 @@
"use client";
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 { import {
CircularProgress, CircularProgress,
FormControl, FormControl,
Grid2 as Grid, Grid2 as Grid,
ListItemButton,
ListItemText,
TextField, TextField,
List,
Autocomplete, Autocomplete,
} from "@mui/material"; } from "@mui/material";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { useDebounce } from "@/hooks/useDebounce"; import { useDebounce } from "@/hooks/useDebounce";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { LichessGameItem } from "./lichess-game-item";
import { LichessRawGameData, NormalizedLichessGameData } from "@/types/lichess";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
interface Props { interface Props {
onSelect: (pgn: string, boardOrientation?: boolean) => void; onSelect: (pgn: string, boardOrientation?: boolean) => void;
} }
// Helper function to normalize Lichess data
const normalizeLichessData = (
data: LichessRawGameData
): NormalizedLichessGameData => ({
id: data.id,
white: {
username: data.players.white.user?.name || "Anonymous",
rating: data.players.white.rating,
title: data.players.white.user?.title,
},
black: {
username: data.players.black.user?.name || "Anonymous",
rating: data.players.black.rating,
title: data.players.black.user?.title,
},
result:
data.status === "draw"
? "1/2-1/2"
: data.winner
? data.winner === "white"
? "1-0"
: "0-1"
: "*",
timeControl: `${Math.floor(data.clock?.initial / 60 || 0)}+${data.clock?.increment || 0}`,
date: new Date(data.createdAt || data.lastMoveAt).toLocaleDateString(),
opening: data.opening?.name,
moves: data.moves?.split(" ").length || 0,
url: `https://lichess.org/${data.id}`,
});
export default function LichessInput({ onSelect }: Props) { export default function LichessInput({ onSelect }: Props) {
const [rawStoredValue, setStoredValues] = useLocalStorage<string>( const [rawStoredValue, setStoredValues] = useLocalStorage<string>(
"lichess-username", "lichess-username",
@@ -147,42 +179,31 @@ export default function LichessInput({ onSelect }: Props) {
No games found. Please check your username. No games found. Please check your username.
</span> </span>
) : ( ) : (
games.map((game) => ( <List sx={{ width: "100%", maxWidth: 800 }}>
<ListItemButton {games.map((game) => {
onClick={() => { const normalizedGame = normalizeLichessData(game);
updateHistory(debouncedUsername); const perspectiveUserColor =
normalizedGame.white.username.toLowerCase() ===
lichessUsername.toLowerCase()
? "white"
: "black";
return (
<LichessGameItem
key={game.id}
{...normalizedGame}
perspectiveUserColor={perspectiveUserColor}
onClick={() => {
updateHistory(debouncedUsername);
const boardOrientation = const boardOrientation =
debouncedUsername.toLowerCase() !== debouncedUsername.toLowerCase() !==
game.players?.black?.user?.name?.toLowerCase(); game.players?.black?.user?.name?.toLowerCase();
onSelect(game.pgn, boardOrientation); onSelect(game.pgn, boardOrientation);
}} }}
style={{ width: 350, maxWidth: 350 }} />
key={game.id} );
> })}
<ListItemText </List>
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={
game.lastMoveAt
? `${capitalize(game.speed || "game")} played at ${new Date(
game.lastMoveAt
)
.toLocaleString()
.slice(0, -3)}`
: undefined
}
slotProps={{
primary: { noWrap: true },
secondary: { noWrap: true },
}}
/>
</ListItemButton>
))
)} )}
</Grid> </Grid>
)} )}

View File

@@ -1,14 +1,46 @@
export interface ChessComGame { interface ChessComPlayerData {
uuid: string;
white: ChessComUser;
black: ChessComUser;
end_time: number;
pgn: string;
time_class: string;
}
export interface ChessComUser {
username: string; username: string;
rating: number; rating: number;
["@id"]: string; result?: string;
title?: string;
}
interface ChessComOpeningData {
name: string;
eco?: string;
}
export interface ChessComRawGameData {
uuid: string;
id: string;
url: string;
pgn: string;
white: ChessComPlayerData;
black: ChessComPlayerData;
result: string;
time_control: string;
end_time: number;
opening?: ChessComOpeningData;
eco?: string;
termination?: string;
}
export interface NormalizedGameData {
id: string;
white: {
username: string;
rating: number;
title?: string;
};
black: {
username: string;
rating: number;
title?: string;
};
result: string;
timeControl: string;
date: string;
opening?: string;
moves: number;
url: string;
} }

View File

@@ -17,18 +17,59 @@ export enum LichessError {
NotFound = "No cloud evaluation available for that position", NotFound = "No cloud evaluation available for that position",
} }
export interface LichessGame { interface LichessPlayerData {
id: string; user: {
speed: string; name: string;
lastMoveAt: number; title?: string;
players?: {
white?: LichessGameUser;
black?: LichessGameUser;
}; };
pgn: string; rating: number;
} }
export interface LichessGameUser { interface LichessClockData {
user?: { id: string; name: string }; initial: number;
rating?: number; increment: number;
totalTime: number;
}
interface LichessOpeningData {
eco: string;
name: string;
ply: number;
}
export interface LichessRawGameData {
id: string;
createdAt: number;
lastMoveAt: number;
status: string;
players: {
white: LichessPlayerData;
black: LichessPlayerData;
};
winner?: "white" | "black";
opening?: LichessOpeningData;
moves: string;
pgn: string;
clock: LichessClockData;
url?: string;
}
export interface NormalizedLichessGameData {
id: string;
white: {
username: string;
rating: number;
title?: string;
};
black: {
username: string;
rating: number;
title?: string;
};
result: string;
timeControl: string;
date: string;
opening?: string;
moves: number;
url: string;
} }