From 8167b9b621b41fb6a2908febb06e9de09794f854 Mon Sep 17 00:00:00 2001 From: titanium_machine <78664175+titaniummachine1@users.noreply.github.com> Date: Wed, 7 May 2025 23:17:06 +0200 Subject: [PATCH] Avatar pictures (#16) * feat(analysis): add Chess.com player avatars to analysis board --- src/components/board/index.tsx | 21 +++++++++++++ src/hooks/usePlayerNames.ts | 45 +++++++++++++++++++++++++++ src/sections/analysis/board/index.tsx | 4 ++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx index 9364c99..9ad2729 100644 --- a/src/components/board/index.tsx +++ b/src/components/board/index.tsx @@ -16,6 +16,7 @@ import { CurrentPosition } from "@/types/eval"; import EvaluationBar from "./evaluationBar"; import CapturedPieces from "./capturedPieces"; import { moveClassificationColors } from "@/lib/chess"; +import Avatar from "@mui/material/Avatar"; export interface Props { id: string; @@ -24,6 +25,8 @@ export interface Props { boardSize?: number; whitePlayer?: string; blackPlayer?: string; + whiteAvatar?: string; + blackAvatar?: string; boardOrientation?: Color; currentPositionAtom?: PrimitiveAtom; showBestMoveArrow?: boolean; @@ -38,6 +41,8 @@ export default function Board({ boardSize, whitePlayer, blackPlayer, + whiteAvatar, + blackAvatar, boardOrientation = Color.White, currentPositionAtom = atom({}), showBestMoveArrow = false, @@ -249,6 +254,14 @@ export default function Board({ columnGap={2} size={12} > + {/* Player avatar, only render if URL is available */} + {(boardOrientation === Color.White ? blackAvatar : whiteAvatar) && ( + + ) } {boardOrientation === Color.White ? blackPlayer : whitePlayer} @@ -298,6 +311,14 @@ export default function Board({ columnGap={2} size={12} > + {/* Player avatar, only render if URL is available */} + { (boardOrientation === Color.White ? whiteAvatar : blackAvatar) && ( + + ) } {boardOrientation === Color.White ? whitePlayer : blackPlayer} diff --git a/src/hooks/usePlayerNames.ts b/src/hooks/usePlayerNames.ts index 916fcbf..7c70faa 100644 --- a/src/hooks/usePlayerNames.ts +++ b/src/hooks/usePlayerNames.ts @@ -1,6 +1,7 @@ import { Chess } from "chess.js"; import { PrimitiveAtom, useAtomValue } from "jotai"; import { useGameDatabase } from "./useGameDatabase"; +import { useState, useEffect } from "react"; export const usePlayersNames = (gameAtom: PrimitiveAtom) => { const game = useAtomValue(gameAtom); @@ -18,10 +19,54 @@ export const usePlayersNames = (gameAtom: PrimitiveAtom) => { const whiteElo = gameFromUrl?.white?.rating || headers.WhiteElo || undefined; const blackElo = gameFromUrl?.black?.rating || headers.BlackElo || undefined; + // Determine if this game came from Chess.com (via PGN header or URL) + const siteHeader = gameFromUrl?.site || headers.Site || ""; + const isChessCom = siteHeader.toLowerCase().includes("chess.com"); + + // Avatars fetched only for Chess.com games + const [whiteAvatar, setWhiteAvatar] = useState(undefined); + const [blackAvatar, setBlackAvatar] = useState(undefined); + + // Fetch white avatar + useEffect(() => { + if (isChessCom && whiteName && whiteName !== "White") { + // Normalize and encode username + const trimmedWhiteName = whiteName.trim().toLowerCase(); + const usernameParam = encodeURIComponent(trimmedWhiteName); + fetch(`https://api.chess.com/pub/player/${usernameParam}`) + .then((res) => res.json()) + .then((data) => setWhiteAvatar(data.avatar || undefined)) + .catch(() => { + setWhiteAvatar(undefined); + }); + } else { + setWhiteAvatar(undefined); + } + }, [isChessCom, whiteName]); + + // Fetch black avatar + useEffect(() => { + if (isChessCom && blackName && blackName !== "Black") { + // Normalize and encode username + const trimmedBlackName = blackName.trim().toLowerCase(); + const usernameParamBlack = encodeURIComponent(trimmedBlackName); + fetch(`https://api.chess.com/pub/player/${usernameParamBlack}`) + .then((res) => res.json()) + .then((data) => setBlackAvatar(data.avatar || undefined)) + .catch(() => { + setBlackAvatar(undefined); + }); + } else { + setBlackAvatar(undefined); + } + }, [isChessCom, blackName]); + return { whiteName, blackName, whiteElo, blackElo, + whiteAvatar, + blackAvatar, }; }; diff --git a/src/sections/analysis/board/index.tsx b/src/sections/analysis/board/index.tsx index e943b7e..a1f0828 100644 --- a/src/sections/analysis/board/index.tsx +++ b/src/sections/analysis/board/index.tsx @@ -17,7 +17,7 @@ export default function BoardContainer() { const screenSize = useScreenSize(); const boardOrientation = useAtomValue(boardOrientationAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); - const { whiteName, whiteElo, blackName, blackElo } = + const { whiteName, whiteElo, blackName, blackElo, whiteAvatar, blackAvatar } = usePlayersNames(gameAtom); const boardSize = useMemo(() => { @@ -40,6 +40,8 @@ export default function BoardContainer() { gameAtom={boardAtom} whitePlayer={whiteElo ? `${whiteName} (${whiteElo})` : whiteName} blackPlayer={blackElo ? `${blackName} (${blackElo})` : blackName} + whiteAvatar={whiteAvatar} + blackAvatar={blackAvatar} boardOrientation={boardOrientation ? Color.White : Color.Black} currentPositionAtom={currentPositionAtom} showBestMoveArrow={showBestMoveArrow}