diff --git a/.eslintrc b/.eslintrc index 7ff09b78..8f3cfac6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,5 @@ { + "root": true, "plugins": ["@typescript-eslint", "prettier", "react", "react-hooks"], "extends": [ "next/core-web-vitals", @@ -18,6 +19,8 @@ "prettier/prettier": "error", "react/react-in-jsx-scope": "off", "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-unused-vars": "off", "@next/next/no-html-link-for-pages": "off", "import/no-named-as-default": "off", diff --git a/.vscode/settings.json b/.vscode/settings.json index a62c7237..1f0128a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "[dockerfile]": { "editor.defaultFormatter": "ms-azuretools.vscode-docker" - } + }, + "editor.tabCompletion": "on", + "github.copilot.nextEditSuggestions.enabled": true } diff --git a/__tests__/analysis/makeMove-fen.test.ts b/__tests__/analysis/makeMove-fen.test.ts deleted file mode 100644 index 5643bb7a..00000000 --- a/__tests__/analysis/makeMove-fen.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { GameTree, GameNode } from 'src/types/base/tree' -import { Chess, PieceSymbol } from 'chess.ts' - -describe('Analysis Page makeMove Logic for FEN Positions', () => { - // Simulate the makeMove function logic from the analysis page - const simulateMakeMove = ( - gameTree: GameTree, - currentNode: GameNode, - move: string, - currentMaiaModel?: string, - ) => { - const chess = new Chess(currentNode.fen) - const moveAttempt = chess.move({ - from: move.slice(0, 2), - to: move.slice(2, 4), - promotion: move[4] ? (move[4] as PieceSymbol) : undefined, - }) - - if (moveAttempt) { - const newFen = chess.fen() - const moveString = - moveAttempt.from + - moveAttempt.to + - (moveAttempt.promotion ? moveAttempt.promotion : '') - const san = moveAttempt.san - - // This is the current logic from the analysis page that we need to fix - if (currentNode.mainChild?.move === moveString) { - return { type: 'navigate', node: currentNode.mainChild } - } else { - // ISSUE: Always creates variation, never main line for first move - const newVariation = gameTree.addVariation( - currentNode, - newFen, - moveString, - san, - currentMaiaModel, - ) - return { type: 'variation', node: newVariation } - } - } - return null - } - - // Fixed version of makeMove logic - const simulateFixedMakeMove = ( - gameTree: GameTree, - currentNode: GameNode, - move: string, - currentMaiaModel?: string, - ) => { - const chess = new Chess(currentNode.fen) - const moveAttempt = chess.move({ - from: move.slice(0, 2), - to: move.slice(2, 4), - promotion: move[4] ? (move[4] as PieceSymbol) : undefined, - }) - - if (moveAttempt) { - const newFen = chess.fen() - const moveString = - moveAttempt.from + - moveAttempt.to + - (moveAttempt.promotion ? moveAttempt.promotion : '') - const san = moveAttempt.san - - if (currentNode.mainChild?.move === moveString) { - // Existing main line move - navigate to it - return { type: 'navigate', node: currentNode.mainChild } - } else if (!currentNode.mainChild) { - // No main child exists - create main line move (FIX) - const newMainMove = gameTree.addMainMove( - currentNode, - newFen, - moveString, - san, - currentMaiaModel, - ) - return { type: 'main', node: newMainMove } - } else { - // Main child exists but different move - create variation - const newVariation = gameTree.addVariation( - currentNode, - newFen, - moveString, - san, - currentMaiaModel, - ) - return { type: 'variation', node: newVariation } - } - } - return null - } - - describe('Current behavior (broken)', () => { - it('incorrectly creates variations for first move from FEN position', () => { - const customFen = - 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 4 4' - const tree = new GameTree(customFen) - const rootNode = tree.getRoot() - - // Simulate making the first move from FEN position - const result = simulateMakeMove(tree, rootNode, 'f3g5') - - // ISSUE: First move incorrectly creates a variation instead of main line - expect(result?.type).toBe('variation') - expect(rootNode.mainChild).toBeNull() // No main line created - expect(rootNode.children.length).toBe(1) - expect(rootNode.getVariations().length).toBe(1) // Created as variation - - // The main line should only contain the root - const mainLine = tree.getMainLine() - expect(mainLine.length).toBe(1) // Only root, no main line progression - }) - - it('shows the problem when making multiple moves from FEN', () => { - const customFen = - 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 4 4' - const tree = new GameTree(customFen) - const rootNode = tree.getRoot() - - // Make first move - const result1 = simulateMakeMove(tree, rootNode, 'f3g5') - expect(result1?.type).toBe('variation') - - // Make second move from same position - const result2 = simulateMakeMove(tree, rootNode, 'f3e5') - expect(result2?.type).toBe('variation') - - // Both moves are variations, no main line established - expect(rootNode.mainChild).toBeNull() - expect(rootNode.getVariations().length).toBe(2) - expect(tree.getMainLine().length).toBe(1) // Still just root - }) - }) - - describe('Fixed behavior', () => { - it('correctly creates main line for first move from FEN position', () => { - const customFen = - 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 4 4' - const tree = new GameTree(customFen) - const rootNode = tree.getRoot() - - // Simulate making the first move from FEN position with fix - const result = simulateFixedMakeMove(tree, rootNode, 'f3g5') - - // FIXED: First move creates main line - expect(result?.type).toBe('main') - expect(rootNode.mainChild).toBeTruthy() // Main line created - expect(rootNode.mainChild?.isMainline).toBe(true) - expect(rootNode.getVariations().length).toBe(0) // No variations yet - - // The main line should now contain root + first move - const mainLine = tree.getMainLine() - expect(mainLine.length).toBe(2) // Root + one move - }) - - it('correctly handles subsequent moves: main line extension and variations', () => { - const customFen = - 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 4 4' - const tree = new GameTree(customFen) - const rootNode = tree.getRoot() - - // First move - should create main line - const result1 = simulateFixedMakeMove(tree, rootNode, 'f3g5') - expect(result1?.type).toBe('main') - const firstMove = result1?.node as GameNode - - // Second move from same position - should create variation - const result2 = simulateFixedMakeMove(tree, rootNode, 'f3e5') - expect(result2?.type).toBe('variation') - - // Third move extending main line - should be main line - const result3 = simulateFixedMakeMove(tree, firstMove, 'd7d6') - expect(result3?.type).toBe('main') - - // Verify final structure - expect(rootNode.mainChild).toBeTruthy() - expect(rootNode.getVariations().length).toBe(1) // One variation - expect(tree.getMainLine().length).toBe(3) // Root + two main moves - }) - - it('correctly navigates to existing moves', () => { - const customFen = - 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 4 4' - const tree = new GameTree(customFen) - const rootNode = tree.getRoot() - - // Create a move first - const result1 = simulateFixedMakeMove(tree, rootNode, 'f3g5') - const existingNode = result1?.node - - // Try the same move again - should navigate to existing node - const result2 = simulateFixedMakeMove(tree, rootNode, 'f3g5') - expect(result2?.type).toBe('navigate') - expect(result2?.node).toBe(existingNode) - - // Structure should remain unchanged - expect(rootNode.children.length).toBe(1) - }) - }) -}) diff --git a/__tests__/analysis/makeMove-variation-fix.test.ts b/__tests__/analysis/makeMove-variation-fix.test.ts deleted file mode 100644 index dc8cca63..00000000 --- a/__tests__/analysis/makeMove-variation-fix.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { GameTree, GameNode } from 'src/types/base/tree' -import { Chess, PieceSymbol } from 'chess.ts' - -describe('makeMove Logic - Variation Continuation Test', () => { - // Test specifically for Kevin's feedback: when we're in a variation and make a move, - // it should continue the variation, not create a new main line - - it('should continue variation when making moves from variation nodes', () => { - const initialFen = - 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' - const gameTree = new GameTree(initialFen) - const root = gameTree.getRoot() - - // Step 1: Create main line move (e2e4) - const mainMove = gameTree.addMainMove( - root, - 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2', - 'e2e4', - 'e4', - ) - - // Step 2: Create a variation from root (d2d4) - const variation = gameTree.addVariation( - root, - 'rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq d6 0 2', - 'd2d4', - 'd4', - ) - - // Verify setup - expect(root.mainChild).toBe(mainMove) - expect(root.mainChild?.isMainline).toBe(true) - expect(variation.isMainline).toBe(false) - expect(root.children).toHaveLength(2) - - // Step 3: Simulate makeMove logic when currentNode is the variation - const simulateMakeMove = (currentNode: GameNode, move: string) => { - const chess = new Chess(currentNode.fen) - const moveAttempt = chess.move({ - from: move.slice(0, 2), - to: move.slice(2, 4), - promotion: move[4] ? (move[4] as PieceSymbol) : undefined, - }) - - if (moveAttempt) { - const newFen = chess.fen() - const moveString = - moveAttempt.from + moveAttempt.to + (moveAttempt.promotion || '') - const san = moveAttempt.san - - // This is the FIXED logic from the analysis page - if (currentNode.mainChild?.move === moveString) { - return { type: 'navigate', node: currentNode.mainChild } - } else if (!currentNode.mainChild && currentNode.isMainline) { - // Only create main line if no main child AND we're on main line - const newMainMove = gameTree.addMainMove( - currentNode, - newFen, - moveString, - san, - ) - return { type: 'main_line', node: newMainMove } - } else { - // Either main child exists but different move, OR we're in variation - create variation - const newVariation = gameTree.addVariation( - currentNode, - newFen, - moveString, - san, - ) - return { type: 'variation', node: newVariation } - } - } - return null - } - - // Step 4: Make move from the variation node (should create another variation, not main line) - const result = simulateMakeMove(variation, 'g1f3') as { - type: 'variation' - node: GameNode - } - - // Assertions - expect(result).not.toBeNull() - expect(result.type).toBe('variation') - expect(result.node.isMainline).toBe(false) - expect(variation.mainChild).toBeNull() // variation should not have gained a main child - expect(variation.children).toHaveLength(1) // should have one child (the move we just made) - expect(variation.children[0].isMainline).toBe(false) // that child should be a variation - expect(variation.children[0].move).toBe('g1f3') - expect(variation.children[0].san).toBe('Nf3') - }) - - it('should create main line when making first move from FEN on main line', () => { - const customFen = - 'r1bqkbnr/pppp1ppp/2n5/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 2 3' - const gameTree = new GameTree(customFen) - const root = gameTree.getRoot() - - const simulateMakeMove = (currentNode: GameNode, move: string) => { - const chess = new Chess(currentNode.fen) - const moveAttempt = chess.move({ - from: move.slice(0, 2), - to: move.slice(2, 4), - promotion: move[4] ? (move[4] as PieceSymbol) : undefined, - }) - - if (moveAttempt) { - const newFen = chess.fen() - const moveString = - moveAttempt.from + moveAttempt.to + (moveAttempt.promotion || '') - const san = moveAttempt.san - - if (currentNode.mainChild?.move === moveString) { - return { type: 'navigate', node: currentNode.mainChild } - } else if (!currentNode.mainChild && currentNode.isMainline) { - const newMainMove = gameTree.addMainMove( - currentNode, - newFen, - moveString, - san, - ) - return { type: 'main_line', node: newMainMove } - } else { - const newVariation = gameTree.addVariation( - currentNode, - newFen, - moveString, - san, - ) - return { type: 'variation', node: newVariation } - } - } - return null - } - - // Make first move from FEN position (should be main line since root is on main line) - const result = simulateMakeMove(root, 'g1f3') as { - type: 'main_line' - node: GameNode - } - - expect(result).not.toBeNull() - expect(result.type).toBe('main_line') - expect(result.node.isMainline).toBe(true) - expect(root.mainChild).toBe(result.node) - }) - - it('should handle complex variation tree correctly', () => { - const initialFen = - 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' - const gameTree = new GameTree(initialFen) - const root = gameTree.getRoot() - - // Create main line: e4 - const e4 = gameTree.addMainMove( - root, - 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1', - 'e2e4', - 'e4', - ) - - // Create variation from root: d4 - const d4 = gameTree.addVariation( - root, - 'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq d3 0 1', - 'd2d4', - 'd4', - ) - - // Create variation from root: Nf3 - const nf3 = gameTree.addVariation( - root, - 'rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1', - 'g1f3', - 'Nf3', - ) - - const simulateMakeMove = (currentNode: GameNode, move: string) => { - const chess = new Chess(currentNode.fen) - const moveAttempt = chess.move({ - from: move.slice(0, 2), - to: move.slice(2, 4), - promotion: move[4] ? (move[4] as PieceSymbol) : undefined, - }) - - if (moveAttempt) { - const newFen = chess.fen() - const moveString = - moveAttempt.from + moveAttempt.to + (moveAttempt.promotion || '') - const san = moveAttempt.san - - if (currentNode.mainChild?.move === moveString) { - return { type: 'navigate', node: currentNode.mainChild } - } else if (!currentNode.mainChild && currentNode.isMainline) { - const newMainMove = gameTree.addMainMove( - currentNode, - newFen, - moveString, - san, - ) - return { type: 'main_line', node: newMainMove } - } else { - const newVariation = gameTree.addVariation( - currentNode, - newFen, - moveString, - san, - ) - return { type: 'variation', node: newVariation } - } - } - return null - } - - // Make move from e4 (main line) - should create main line continuation - const e4Continue = simulateMakeMove(e4, 'e7e5') as { - type: 'main_line' - node: GameNode - } - expect(e4Continue).not.toBeNull() - expect(e4Continue.type).toBe('main_line') - expect(e4Continue.node.isMainline).toBe(true) - - // Make move from d4 (variation) - should create variation continuation - const d4Continue = simulateMakeMove(d4, 'g8f6') as { - type: 'variation' - node: GameNode - } - expect(d4Continue.type).toBe('variation') - expect(d4Continue.node.isMainline).toBe(false) - - // Make move from Nf3 (variation) - should create variation continuation - const nf3Continue = simulateMakeMove(nf3, 'e7e5') as { - type: 'variation' - node: GameNode - } - expect(nf3Continue.type).toBe('variation') - expect(nf3Continue.node.isMainline).toBe(false) - - // Verify tree structure - expect(root.children).toHaveLength(3) // e4, d4, Nf3 - expect(e4.children).toHaveLength(1) // e5 (main line) - expect(d4.children).toHaveLength(1) // Nf6 (variation) - expect(nf3.children).toHaveLength(1) // e5 (variation) - - expect(e4.mainChild).toBe(e4Continue.node) - expect(d4.mainChild).toBeNull() // variations don't have main children - expect(nf3.mainChild).toBeNull() // variations don't have main children - }) -}) diff --git a/__tests__/api/active-users.test.ts b/__tests__/api/active-users.test.ts deleted file mode 100644 index 2564eff9..00000000 --- a/__tests__/api/active-users.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createMocks } from 'node-mocks-http' -import handler from 'src/pages/api/active-users' - -global.fetch = jest.fn() - -describe('/api/active-users', () => { - beforeEach(() => { - jest.clearAllMocks() - delete process.env.POSTHOG_PROJECT_ID - delete process.env.POSTHOG_API_KEY - }) - - it('should return 405 for non-GET requests', async () => { - const { req, res } = createMocks({ - method: 'POST', - }) - - await handler(req, res) - - expect(res._getStatusCode()).toBe(405) - const data = JSON.parse(res._getData()) - expect(data.success).toBe(false) - expect(data.error).toBe('Method not allowed') - }) -}) diff --git a/__tests__/api/home/activeUsers.test.ts b/__tests__/api/home/activeUsers.test.ts deleted file mode 100644 index 64cf8cb8..00000000 --- a/__tests__/api/home/activeUsers.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getActiveUserCount } from 'src/api/home/activeUsers' - -// Mock fetch for API calls -global.fetch = jest.fn() - -describe('getActiveUserCount', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should return a positive number', async () => { - // Mock successful API response - ;(fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => ({ - activeUsers: 15, - success: true, - }), - }) - - const count = await getActiveUserCount() - expect(count).toBeGreaterThanOrEqual(0) - expect(Number.isInteger(count)).toBe(true) - }) - - it('should call the internal API endpoint', async () => { - // Mock successful API response - ;(fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => ({ - activeUsers: 10, - success: true, - }), - }) - - const count = await getActiveUserCount() - - expect(fetch).toHaveBeenCalledWith('/api/active-users') - expect(count).toBe(10) - }) -}) diff --git a/__tests__/components/Analysis/AnalyzeEntireGame.test.tsx b/__tests__/components/Analysis/AnalyzeEntireGame.test.tsx deleted file mode 100644 index 85360618..00000000 --- a/__tests__/components/Analysis/AnalyzeEntireGame.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import { AnalysisConfigModal } from 'src/components/Analysis/AnalysisConfigModal' -import { AnalysisNotification } from 'src/components/Analysis/AnalysisNotification' -import '@testing-library/jest-dom' - -// Mock framer-motion to avoid animation issues in tests -jest.mock('framer-motion', () => ({ - motion: { - div: ({ children, ...props }: any) =>
Higher depths provide more accurate analysis but take longer to - complete. You can cancel the analysis at any time. Currently, - analysis only persists until you close the tab, but we are working - on a persistent analysis feature! + complete. You can cancel the analysis at any time. Analysis will + persist even after you close the tab,
diff --git a/src/components/Analysis/AnalysisGameList.tsx b/src/components/Analysis/AnalysisGameList.tsx index 5c9007ba..66611a4c 100644 --- a/src/components/Analysis/AnalysisGameList.tsx +++ b/src/components/Analysis/AnalysisGameList.tsx @@ -11,16 +11,13 @@ import { motion } from 'framer-motion' import { Tournament } from 'src/components' import { FavoriteModal } from 'src/components/Common/FavoriteModal' import { AnalysisListContext } from 'src/contexts' -import { getAnalysisGameList } from 'src/api' -import { getCustomAnalysesAsWebGames } from 'src/lib/customAnalysis' +import { fetchMaiaGameList } from 'src/api' import { getFavoritesAsWebGames, addFavoriteGame, removeFavoriteGame, - updateFavoriteName, - isFavoriteGame, } from 'src/lib/favorites' -import { AnalysisWebGame } from 'src/types' +import { MaiaGameListEntry } from 'src/types' import { useRouter } from 'next/router' interface GameData { @@ -28,28 +25,26 @@ interface GameData { maia_name: string result: string player_color: 'white' | 'black' + is_favorited?: boolean + custom_name?: string } interface AnalysisGameListProps { currentId: string[] | null - loadNewTournamentGame: ( + loadNewWorldChampionshipGame: ( newId: string[], setCurrentMove?: Dispatch- {selected === 'play' || selected === 'hb' + {selected === 'play' || + selected === 'hb' || + selected === 'favorites' ? (currentPage - 1) * 25 + index + 1 : index + 1}
@@ -551,12 +719,9 @@ export const AnalysisGameList: React.FC+ {player.name} +
+ + {player.rating ? <>({player.rating})> : null} + +1
+ ) : game.termination?.winner !== 'none' ? ( +0
+ ) : game.termination === undefined ? ( + <>> + ) : ( +½
+ )} ++ {broadcastController.broadcastState.isConnecting + ? 'Loading games...' + : broadcastController.currentRound?.ongoing + ? 'No games available' + : 'Round not started yet'} +
+{index + 1}
++ Watch on{' '} + + Lichess + +
+{player.name}
+ + {player.rating ? <>({player.rating})> : null} + +1
+ ) : game.termination?.winner !== 'none' ? ( +0
+ ) : game.termination === undefined ? ( + <>> + ) : ( +½
+ )} ++ {message} +
+ ) : null}- {name ?? 'Unknown'} {rating ? `(${rating})` : null} +
+ {name ?? 'Unknown'}{' '} + + {rating ? `(${rating})` : null} +
+ {currentFen && ( +1
) : termination === undefined ? ( diff --git a/src/components/Common/ResignationConfirmModal.tsx b/src/components/Common/ResignationConfirmModal.tsx new file mode 100644 index 00000000..ec6f8175 --- /dev/null +++ b/src/components/Common/ResignationConfirmModal.tsx @@ -0,0 +1,46 @@ +interface ResignationConfirmModalProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void +} + +export const ResignationConfirmModal: React.FC< + ResignationConfirmModalProps +> = ({ isOpen, onClose, onConfirm }) => { + if (!isOpen) return null + + const handleConfirm = () => { + onConfirm() + onClose() + } + + return ( ++ Are you sure you want to resign this game? This action cannot be + undone. +
+ ++ {description} +
Learn More
- - keyboard_double_arrow_down - -+ Sign in with: +
++
+
+
+
- Maia Chess is in open beta. You now have full access to the platform! -
-+ Maia Chess is in open beta. You now have full access to the + platform! +
+Loading live game...
+{error}
+No live game available
+{index + 1}
- -+
{index + 1}
+{display_name} {index == 0 && '👑'}
- -{elo}
- {isPopupVisible && stats && ( -- {display_name}'s {type}{' '} - Statistics -
- - - open_in_new - - -Rating
- {stats[ratingKey]} -Highest
- {stats[highestRatingKey]} -Games
- {stats[gamesKey]} +{elo}
+ {isPopupVisible && stats && ( ++ {display_name}'s {type}{' '} + Statistics +
+ + + open_in_new + +Win %
- - {((stats[gamesWonKey] / stats[gamesKey]) * 100).toFixed(0)}% - +Rating
+ {stats[ratingKey]} +Highest
+ + {stats[highestRatingKey]} + +Games
+ {stats[gamesKey]} +Win %
+ + {((stats[gamesWonKey] / stats[gamesKey]) * 100).toFixed(0)}% + +- Number of Drills: {drillCount} -
- setDrillCount(parseInt(e.target.value) || 5)} - className="w-full accent-human-4" - /> -- {drillCount <= selections.length - ? `You'll play ${drillCount} of your selected openings` - : selections.length > 0 - ? `Each opening played at least once, with ${drillCount - selections.length} repeats` - : 'Total number of opening drills to complete'} -
-SELECT PIECE
-+
{userName ? `${userName}'s Games` : 'Your Games'}
- {selected === 'play' || selected === 'hb' +
+ {selected === 'play' || + selected === 'hb' || + selected === 'favorites' ? (currentPage - 1) * 25 + index + 1 : index + 1}
- {game.label} +
+ {displayName}
{selected === 'favorites' && (game.type === 'hand' || game.type === 'brain') && ( - + {game.type === 'hand' ? 'hand_gesture' : 'neurology'} @@ -450,7 +579,7 @@ export const GameList = ({ e.stopPropagation() handleFavoriteGame(game) }} - className="flex items-center justify-center text-secondary transition hover:text-primary" + className="flex items-center justify-center text-white/60 transition-colors duration-200 hover:text-white/90" title="Edit favourite" > @@ -464,10 +593,10 @@ export const GameList = ({ e.stopPropagation() handleFavoriteGame(game) }} - className={`flex items-center justify-center transition ${ + className={`flex items-center justify-center transition-colors duration-200 ${ isFavorited ? 'text-yellow-400 hover:text-yellow-300' - : 'text-secondary hover:text-primary' + : 'text-white/60 hover:text-white/90' }`} title={ isFavorited ? 'Edit favourite' : 'Add to favourites' @@ -480,7 +609,7 @@ export const GameList = ({ )} -+
{game.result.replace('1/2', '½').replace('1/2', '½')}
{label}
@@ -577,7 +714,7 @@ function Header({
{selected === name && (
{name} {name} Rating Rating Highest Highest Games Games Hours
+
Wins: {wins}{' '}
-
+
({Math.round((wins * 100) / data.games) || 0}%)
+
Draws: {draws}{' '}
-
+
({Math.round((draws * 100) / data.games) || 0}%)
+
Losses: {losses}{' '}
-
+
({Math.round((losses * 100) / data.games) || 0}%)
+
Choose your preferred chessboard style. Changes will apply to all
chess boards across the platform.
+
Manage your locally stored Maia chess engine model. The model is
downloaded once and stored in your browser for offline use.
Model Status
- {statusDisplay.text}
-
+
+ warning
+
+ IndexedDB storage is not supported in your browser. Model
+ management features are unavailable.
+ Model Status
+ {statusDisplay.text}
+
-
- warning
-
- IndexedDB storage is not supported in your browser. Model
- management features are unavailable.
-
- Customize your Maia Chess experience. All settings are saved locally
- in your browser.
+
+ Customize your Maia Chess experience. Settings are saved locally in
+ your browser.
+
+ Enable Move Sounds
+
+
Play sounds when chess pieces are moved or captured
Test sounds: Test sounds:Chessboard Theme
-
+ Chessboard Theme
+
+ Maia Neural Network Model
-
+ Maia Neural Network Model
+
+ Storage Information
-
+ Storage Information
+
+ Settings
+
+ settings
+
+ Settings
Sound Settings
+ Sound Settings