style : enhance load game UI

This commit is contained in:
GuillaumeSD
2025-06-16 05:01:52 +02:00
parent 5e2d944513
commit f906c81c67
10 changed files with 52 additions and 72 deletions

37
package-lock.json generated
View File

@@ -3286,9 +3286,9 @@
} }
}, },
"node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": { "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -4108,9 +4108,9 @@
} }
}, },
"node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4248,9 +4248,9 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4330,9 +4330,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4457,9 +4457,9 @@
} }
}, },
"node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5512,10 +5512,11 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"

View File

@@ -39,6 +39,9 @@ if (
"RuntimeError: Out of bounds memory access (evaluating 'n.apply(null,arguments)')", "RuntimeError: Out of bounds memory access (evaluating 'n.apply(null,arguments)')",
"Uncaught RuntimeError: Aborted(CompileError: WebAssembly.instantiate():", "Uncaught RuntimeError: Aborted(CompileError: WebAssembly.instantiate():",
"Uncaught RangeError: WebAssembly.Memory(): could not allocate memory", "Uncaught RangeError: WebAssembly.Memory(): could not allocate memory",
"Aborted(NetworkError: Failed to execute 'send' on 'XMLHttpRequest'",
"Aborted(CompileError: WebAssembly.Module doesn't parse at byte",
"Aborted(NetworkError: A network error occurred",
], ],
}); });
} }

View File

@@ -1,7 +1,6 @@
import { ChessComGame } from "@/types/chessCom"; import { ChessComGame } from "@/types/chessCom";
import { getPaddedNumber } from "./helpers"; import { getPaddedNumber } from "./helpers";
import { LoadedGame } from "@/types/game"; import { LoadedGame } from "@/types/game";
import { Chess } from "chess.js";
export const getChessComUserRecentGames = async ( export const getChessComUserRecentGames = async (
username: string, username: string,
@@ -63,8 +62,8 @@ export const getChessComUserAvatar = async (
}; };
const formatChessComGame = (data: ChessComGame): LoadedGame => { const formatChessComGame = (data: ChessComGame): LoadedGame => {
const game = new Chess(); const result = data.pgn.match(/\[Result "(.*?)"]/)?.[1];
game.loadPgn(data.pgn); const movesNb = data.pgn.match(/\d+?\. /g)?.length;
return { return {
id: data.uuid || data.url?.split("/").pop() || data.id, id: data.uuid || data.url?.split("/").pop() || data.id,
@@ -79,12 +78,12 @@ const formatChessComGame = (data: ChessComGame): LoadedGame => {
rating: data.black?.rating || 0, rating: data.black?.rating || 0,
title: data.black?.title, title: data.black?.title,
}, },
result: game.getHeaders().Result, result,
timeControl: getGameTimeControl(data), timeControl: getGameTimeControl(data),
date: data.end_time date: data.end_time
? new Date(data.end_time * 1000).toLocaleDateString() ? new Date(data.end_time * 1000).toLocaleDateString()
: new Date().toLocaleDateString(), : new Date().toLocaleDateString(),
movesNb: game.history().length, movesNb: movesNb ? movesNb * 2 : undefined,
url: data.url, url: data.url,
}; };
}; };

View File

@@ -85,7 +85,7 @@ export default function ChessComInput({ onSelect }: Props) {
return ( return (
<> <>
<FormControl sx={{ m: 1, width: 300 }}> <FormControl sx={{ my: 1, width: 300 }}>
<Autocomplete <Autocomplete
freeSolo freeSolo
options={storedValues} options={storedValues}
@@ -147,7 +147,7 @@ export default function ChessComInput({ onSelect }: Props) {
No games found. Please check your username. No games found. Please check your username.
</span> </span>
) : ( ) : (
<List sx={{ width: "100%", maxWidth: 800 }}> <List sx={{ width: "100%" }}>
{games.map((game) => { {games.map((game) => {
const perspectiveUserColor = const perspectiveUserColor =
game.white.name.toLowerCase() === game.white.name.toLowerCase() ===
@@ -161,11 +161,11 @@ export default function ChessComInput({ onSelect }: Props) {
game={game} game={game}
perspectiveUserColor={perspectiveUserColor} perspectiveUserColor={perspectiveUserColor}
onClick={() => { onClick={() => {
updateHistory(debouncedUsername);
const boardOrientation = const boardOrientation =
debouncedUsername.toLowerCase() !== debouncedUsername.toLowerCase() !==
game.black?.name?.toLowerCase(); game.black?.name?.toLowerCase();
onSelect(game.pgn, boardOrientation); onSelect(game.pgn, boardOrientation);
updateHistory(debouncedUsername);
}} }}
/> />
); );

View File

@@ -1,4 +1,3 @@
import { Icon } from "@iconify/react";
import { Chip, Theme, Tooltip, useTheme } from "@mui/material"; import { Chip, Theme, Tooltip, useTheme } from "@mui/material";
import React from "react"; import React from "react";
@@ -13,7 +12,7 @@ export default function GameResultChip({
}: Props) { }: Props) {
const theme = useTheme(); const theme = useTheme();
const { label, color, bgColor, icon } = getResultSpecs( const { label, color, bgColor } = getResultSpecs(
theme, theme,
perspectiveUserColor, perspectiveUserColor,
result result
@@ -22,14 +21,13 @@ export default function GameResultChip({
return ( return (
<Tooltip title={label}> <Tooltip title={label}>
<Chip <Chip
icon={icon}
label={result} label={result}
size="small" size="small"
sx={{ sx={{
color, color,
backgroundColor: bgColor, backgroundColor: bgColor,
fontWeight: "600", fontWeight: "600",
minWidth: icon ? 65 : 40, minWidth: { sm: 40 },
border: `1px solid ${color}20`, border: `1px solid ${color}20`,
"& .MuiChip-icon": { "& .MuiChip-icon": {
color: color, color: color,
@@ -53,7 +51,6 @@ const getResultSpecs = (
label: result === "1-0" ? "White won" : "Black won", label: result === "1-0" ? "White won" : "Black won",
color: theme.palette.success.main, color: theme.palette.success.main,
bgColor: `${theme.palette.success.main}1A`, bgColor: `${theme.palette.success.main}1A`,
icon: <Icon icon="material-symbols:emoji-events" />,
}; };
} }
@@ -73,7 +70,6 @@ const getResultSpecs = (
label: "Draw", label: "Draw",
color: theme.palette.info.main, color: theme.palette.info.main,
bgColor: `${theme.palette.info.main}1A`, bgColor: `${theme.palette.info.main}1A`,
icon: <Icon icon="material-symbols:handshake" />,
}; };
} }
@@ -81,6 +77,5 @@ const getResultSpecs = (
label: "Game in Progress", label: "Game in Progress",
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
bgColor: theme.palette.action.hover, bgColor: theme.palette.action.hover,
icon: <Icon icon="material-symbols:play-circle-outline" />,
}; };
}; };

View File

@@ -14,7 +14,7 @@ import GameResultChip from "./gameResultChip";
interface Props { interface Props {
game: LoadedGame; game: LoadedGame;
onClick?: () => void; onClick: () => void;
perspectiveUserColor: "white" | "black"; perspectiveUserColor: "white" | "black";
} }
@@ -38,34 +38,34 @@ export const GameItem: React.FC<Props> = ({
transition: "all 0.2s ease-in-out", transition: "all 0.2s ease-in-out",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
transform: "translateY(-1px)",
boxShadow: theme.shadows[3], boxShadow: theme.shadows[3],
}, },
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
cursor: onClick ? "pointer" : "default", cursor: "pointer",
}} }}
onClick={onClick} onClick={onClick}
> >
<ListItemText <ListItemText
disableTypography
primary={ primary={
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
flexWrap: "wrap", gap: { xs: 1, sm: 1.5 },
gap: 1.5,
mb: 1, mb: 1,
}} }}
> >
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="span" component="span"
noWrap
sx={{ sx={{
fontWeight: "700", fontWeight: "700",
color: whiteWon color: whiteWon
? theme.palette.success.main ? theme.palette.success.main
: theme.palette.text.primary, : theme.palette.text.primary,
opacity: blackWon ? 0.7 : 1, opacity: whiteWon ? 1 : blackWon ? 0.7 : 0.8,
}} }}
> >
{formatPlayerName(white)} ({white.rating}) {formatPlayerName(white)} ({white.rating})
@@ -74,7 +74,10 @@ export const GameItem: React.FC<Props> = ({
<Typography <Typography
variant="body2" variant="body2"
component="span" component="span"
sx={{ color: theme.palette.text.secondary, fontWeight: "500" }} sx={{
color: theme.palette.text.secondary,
fontWeight: "500",
}}
> >
vs vs
</Typography> </Typography>
@@ -82,12 +85,13 @@ export const GameItem: React.FC<Props> = ({
<Typography <Typography
variant="subtitle1" variant="subtitle1"
component="span" component="span"
noWrap
sx={{ sx={{
fontWeight: "700", fontWeight: "700",
color: blackWon color: blackWon
? theme.palette.success.main ? theme.palette.success.main
: theme.palette.text.primary, : theme.palette.text.primary,
opacity: whiteWon ? 0.7 : 1, opacity: blackWon ? 1 : whiteWon ? 0.7 : 0.8,
}} }}
> >
{formatPlayerName(black)} ({black.rating}) {formatPlayerName(black)} ({black.rating})
@@ -103,7 +107,6 @@ export const GameItem: React.FC<Props> = ({
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexWrap: "wrap",
gap: 1, gap: 1,
alignItems: "center", alignItems: "center",
}} }}
@@ -113,30 +116,7 @@ export const GameItem: React.FC<Props> = ({
<DateChip date={date} /> <DateChip date={date} />
</Box> </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> </ListItem>
); );
}; };

View File

@@ -9,7 +9,7 @@ export default function MovesNbChip({ movesNb }: Props) {
if (!movesNb) return null; if (!movesNb) return null;
return ( return (
<Tooltip title="Number of Moves"> <Tooltip title="Number of Moves" sx={{ overflow: "hidden" }}>
<Chip <Chip
icon={<Icon icon="heroicons:hashtag-20-solid" />} icon={<Icon icon="heroicons:hashtag-20-solid" />}
label={`${Math.ceil(movesNb / 2)} moves`} label={`${Math.ceil(movesNb / 2)} moves`}

View File

@@ -37,7 +37,7 @@ export default function GamePgnInput({ pgn, setPgn }: Props) {
component="label" component="label"
startIcon={<Icon icon="material-symbols:upload" />} startIcon={<Icon icon="material-symbols:upload" />}
> >
Choose PGN File Upload PGN File
<input type="file" hidden accept=".pgn" onChange={handleFileChange} /> <input type="file" hidden accept=".pgn" onChange={handleFileChange} />
</Button> </Button>
</FormControl> </FormControl>

View File

@@ -84,7 +84,7 @@ export default function LichessInput({ onSelect }: Props) {
return ( return (
<> <>
<FormControl sx={{ m: 1, width: 300 }}> <FormControl sx={{ my: 1, width: 300 }}>
<Autocomplete <Autocomplete
freeSolo freeSolo
options={storedValues} options={storedValues}
@@ -146,7 +146,7 @@ export default function LichessInput({ onSelect }: Props) {
No games found. Please check your username. No games found. Please check your username.
</span> </span>
) : ( ) : (
<List sx={{ width: "100%", maxWidth: 800 }}> <List sx={{ width: "100%" }}>
{games.map((game) => { {games.map((game) => {
const perspectiveUserColor = const perspectiveUserColor =
game.white.name.toLowerCase() === game.white.name.toLowerCase() ===

View File

@@ -91,12 +91,14 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
open={open} open={open}
onClose={handleClose} onClose={handleClose}
maxWidth="md" maxWidth="md"
fullWidth
slotProps={{ slotProps={{
paper: { paper: {
sx: { sx: {
position: "fixed", position: "fixed",
top: 0, top: 0,
width: "calc(100% - 10px)",
marginY: { xs: "3vh", sm: 5 },
maxHeight: { xs: "calc(100% - 5vh)", sm: "calc(100% - 64px)" },
}, },
}, },
}} }}
@@ -104,7 +106,7 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
<DialogTitle marginY={1} variant="h5"> <DialogTitle marginY={1} variant="h5">
{setGame ? "Load a game" : "Add a game to your database"} {setGame ? "Load a game" : "Add a game to your database"}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent sx={{ padding: { xs: 2, md: 3 } }}>
<Grid <Grid
container container
marginTop={1} marginTop={1}
@@ -112,7 +114,7 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
justifyContent="start" justifyContent="start"
rowGap={2} rowGap={2}
> >
<FormControl sx={{ m: 1, width: 150 }}> <FormControl sx={{ my: 1, mr: 2, width: 150 }}>
<InputLabel id="dialog-select-label">Game origin</InputLabel> <InputLabel id="dialog-select-label">Game origin</InputLabel>
<Select <Select
labelId="dialog-select-label" labelId="dialog-select-label"