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/__tests__/lib/favorites.test.ts b/__tests__/lib/favorites.test.ts index 4b56a3ff..50b1a2a3 100644 --- a/__tests__/lib/favorites.test.ts +++ b/__tests__/lib/favorites.test.ts @@ -31,6 +31,16 @@ Object.defineProperty(window, 'localStorage', { value: localStorageMock, }) +// Mock the API functions to test fallback to localStorage +jest.mock('src/api/analysis/analysis', () => ({ + updateGameMetadata: jest + .fn() + .mockRejectedValue(new Error('API not available')), + getAnalysisGameList: jest + .fn() + .mockRejectedValue(new Error('API not available')), +})) + describe('favorites', () => { beforeEach(() => { localStorageMock.clear() @@ -44,66 +54,66 @@ describe('favorites', () => { } describe('addFavoriteGame', () => { - it('should add a game to favorites with default name', () => { - const favorite = addFavoriteGame(mockGame) + it('should add a game to favorites with default name', async () => { + const favorite = await addFavoriteGame(mockGame) expect(favorite.id).toBe(mockGame.id) expect(favorite.customName).toBe(mockGame.label) expect(favorite.originalLabel).toBe(mockGame.label) - expect(isFavoriteGame(mockGame.id)).toBe(true) + expect(await isFavoriteGame(mockGame.id)).toBe(true) }) - it('should add a game to favorites with custom name', () => { + it('should add a game to favorites with custom name', async () => { const customName = 'My Best Game' - const favorite = addFavoriteGame(mockGame, customName) + const favorite = await addFavoriteGame(mockGame, customName) expect(favorite.customName).toBe(customName) expect(favorite.originalLabel).toBe(mockGame.label) }) - it('should update existing favorite when added again', () => { - addFavoriteGame(mockGame, 'First Name') - addFavoriteGame(mockGame, 'Updated Name') + it('should update existing favorite when added again', async () => { + await addFavoriteGame(mockGame, 'First Name') + await addFavoriteGame(mockGame, 'Updated Name') - const favorites = getFavoriteGames() + const favorites = await getFavoriteGames() expect(favorites).toHaveLength(1) expect(favorites[0].customName).toBe('Updated Name') }) }) describe('removeFavoriteGame', () => { - it('should remove a game from favorites', () => { - addFavoriteGame(mockGame) - expect(isFavoriteGame(mockGame.id)).toBe(true) + it('should remove a game from favorites', async () => { + await addFavoriteGame(mockGame) + expect(await isFavoriteGame(mockGame.id)).toBe(true) - removeFavoriteGame(mockGame.id) - expect(isFavoriteGame(mockGame.id)).toBe(false) + await removeFavoriteGame(mockGame.id, mockGame.type) + expect(await isFavoriteGame(mockGame.id)).toBe(false) }) }) describe('updateFavoriteName', () => { - it('should update favorite name', () => { - addFavoriteGame(mockGame, 'Original Name') - updateFavoriteName(mockGame.id, 'New Name') + it('should update favorite name', async () => { + await addFavoriteGame(mockGame, 'Original Name') + await updateFavoriteName(mockGame.id, 'New Name', mockGame.type) - const favorite = getFavoriteGame(mockGame.id) + const favorite = await getFavoriteGame(mockGame.id) expect(favorite?.customName).toBe('New Name') }) - it('should do nothing if favorite does not exist', () => { - const initialFavorites = getFavoriteGames() - updateFavoriteName('non-existent', 'New Name') + it('should do nothing if favorite does not exist', async () => { + const initialFavorites = await getFavoriteGames() + await updateFavoriteName('non-existent', 'New Name') - expect(getFavoriteGames()).toEqual(initialFavorites) + expect(await getFavoriteGames()).toEqual(initialFavorites) }) }) describe('getFavoritesAsWebGames', () => { - it('should convert favorites to web games', () => { + it('should convert favorites to web games', async () => { const customName = 'Custom Game Name' - addFavoriteGame(mockGame, customName) + await addFavoriteGame(mockGame, customName) - const webGames = getFavoritesAsWebGames() + const webGames = await getFavoritesAsWebGames() expect(webGames).toHaveLength(1) expect(webGames[0].label).toBe(customName) expect(webGames[0].id).toBe(mockGame.id) @@ -111,7 +121,7 @@ describe('favorites', () => { }) describe('storage limits', () => { - it('should limit favorites to 100 entries', () => { + it('should limit favorites to 100 entries', async () => { // Add 101 favorites for (let i = 0; i < 101; i++) { const game: AnalysisWebGame = { @@ -120,10 +130,10 @@ describe('favorites', () => { label: `Game ${i}`, result: '1-0', } - addFavoriteGame(game) + await addFavoriteGame(game) } - const favorites = getFavoriteGames() + const favorites = await getFavoriteGames() expect(favorites).toHaveLength(100) // Latest should be at the top expect(favorites[0].id).toBe('game-100') diff --git a/package-lock.json b/package-lock.json index 02d090b7..f764ced1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,12 +45,13 @@ "@types/node": "17.0.8", "@types/react": "19.0.8", "@types/react-dom": "^19.1.6", - "@typescript-eslint/eslint-plugin": "^5.9.1", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "autoprefixer": "^10.4.20", "babel-loader": "^8.2.3", - "eslint": "8.6.0", + "eslint": "^8.57.0", "eslint-config-next": "15.1.6", - "eslint-config-prettier": "^8.3.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^5.0.0", @@ -63,7 +64,7 @@ "prettier-plugin-tailwindcss": "^0.6.6", "sass-loader": "^12.4.0", "tailwindcss": "^3.4.10", - "typescript": "^5.1.6" + "typescript": "^5.8.3" } }, "node_modules/@adobe/css-tools": { @@ -824,7 +825,6 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, - "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -849,15 +849,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -872,6 +871,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", @@ -904,28 +912,39 @@ "license": "MIT" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.3", @@ -2382,13 +2401,6 @@ "@types/react": "^19.0.0" } }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2427,135 +2439,152 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.39.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", + "debug": "^4.3.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, - "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2563,31 +2592,83 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { @@ -2595,7 +2676,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2604,63 +2684,57 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.39.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2957,7 +3031,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3044,16 +3117,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3141,8 +3204,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/aria-query": { "version": "5.3.0", @@ -3194,16 +3256,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -4546,19 +4598,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4668,20 +4707,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -4913,51 +4938,50 @@ } }, "node_modules/eslint": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.6.0.tgz", - "integrity": "sha512-UvxdOJ7mXFlw7iuHZA4jmzPaUqIw54mZrv+XPYKNbKdLR0et4rf60lIZUU9kiNtnzzMzGWxMV+tQ7uG7JG8DPw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "license": "MIT", "dependencies": { - "@eslint/eslintrc": "^1.0.5", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.0", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.1.0", - "espree": "^9.3.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.2.0", - "semver": "^7.2.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -5033,11 +5057,10 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", - "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, - "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5306,6 +5329,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -5320,39 +5344,11 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -5383,27 +5379,50 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/espree": { @@ -5411,7 +5430,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -5927,13 +5945,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true, - "license": "MIT" - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -6129,7 +6140,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -6157,27 +6167,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", @@ -6492,7 +6481,6 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -6961,6 +6949,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -8266,7 +8263,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9450,13 +9446,6 @@ "dev": true, "license": "MIT" }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9938,7 +9927,6 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -10074,16 +10062,6 @@ "dev": true, "license": "ISC" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10490,16 +10468,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10992,19 +10960,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -11108,7 +11063,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -12757,6 +12711,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -12806,29 +12772,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12857,7 +12800,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -13159,13 +13101,6 @@ "dev": true, "license": "MIT" }, - "node_modules/v8-compile-cache": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", - "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", - "dev": true, - "license": "MIT" - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 8af34220..761fad54 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,13 @@ "@types/node": "17.0.8", "@types/react": "19.0.8", "@types/react-dom": "^19.1.6", - "@typescript-eslint/eslint-plugin": "^5.9.1", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "autoprefixer": "^10.4.20", "babel-loader": "^8.2.3", - "eslint": "8.6.0", + "eslint": "^8.57.0", "eslint-config-next": "15.1.6", - "eslint-config-prettier": "^8.3.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^5.0.0", @@ -67,7 +68,7 @@ "prettier-plugin-tailwindcss": "^0.6.6", "sass-loader": "^12.4.0", "tailwindcss": "^3.4.10", - "typescript": "^5.1.6" + "typescript": "^5.8.3" }, "overrides": { "@types/react": "19.0.8" diff --git a/src/api/analysis/analysis.ts b/src/api/analysis/analysis.ts index 48c1b172..d7c00de3 100644 --- a/src/api/analysis/analysis.ts +++ b/src/api/analysis/analysis.ts @@ -716,6 +716,36 @@ export const getEngineAnalysis = async ( return res.json() } +export interface UpdateGameMetadataRequest { + custom_name?: string + is_favorited?: boolean +} + +export const updateGameMetadata = async ( + gameType: 'custom' | 'play' | 'hand' | 'brain', + gameId: string, + metadata: UpdateGameMetadataRequest, +): Promise => { + const res = await fetch( + buildUrl(`analysis/update_metadata/${gameType}/${gameId}`), + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(metadata), + }, + ) + + if (res.status === 401) { + throw new Error('Unauthorized') + } + + if (!res.ok) { + throw new Error('Failed to update game metadata') + } +} + export interface StoreCustomGameRequest { name?: string pgn?: string diff --git a/src/components/Analysis/AnalysisGameList.tsx b/src/components/Analysis/AnalysisGameList.tsx index 18e90575..a523fbd1 100644 --- a/src/components/Analysis/AnalysisGameList.tsx +++ b/src/components/Analysis/AnalysisGameList.tsx @@ -28,6 +28,8 @@ interface GameData { maia_name: string result: string player_color: 'white' | 'black' + is_favorited?: boolean + custom_name?: string } interface AnalysisGameListProps { @@ -84,15 +86,14 @@ export const AnalysisGameList: React.FC = ({ play: {}, hand: {}, brain: {}, + favorites: {}, custom: {}, }) - const [favoriteGames, setFavoriteGames] = useState(() => { - if (typeof window !== 'undefined') { - return getFavoritesAsWebGames() - } - return [] - }) + const [favoriteGames, setFavoriteGames] = useState([]) + const [favoritedGameIds, setFavoritedGameIds] = useState>( + new Set(), + ) const [hbSubsection, setHbSubsection] = useState<'hand' | 'brain'>('hand') // Modal state for favoriting @@ -102,7 +103,16 @@ export const AnalysisGameList: React.FC = ({ }>({ isOpen: false, game: null }) useEffect(() => { - setFavoriteGames(getFavoritesAsWebGames()) + // Load favorites asynchronously + getFavoritesAsWebGames() + .then((favorites) => { + setFavoriteGames(favorites) + setFavoritedGameIds(new Set(favorites.map((f) => f.id))) + }) + .catch(() => { + setFavoriteGames([]) + setFavoritedGameIds(new Set()) + }) }, [refreshTrigger]) useEffect(() => { @@ -126,6 +136,7 @@ export const AnalysisGameList: React.FC = ({ custom: {}, lichess: {}, tournament: {}, + favorites: {}, }) const [totalPagesCache, setTotalPagesCache] = useState<{ @@ -141,6 +152,7 @@ export const AnalysisGameList: React.FC = ({ custom: 1, lichess: 1, tournament: 1, + favorites: 1, }) const listKeys = useMemo(() => { @@ -200,8 +212,7 @@ export const AnalysisGameList: React.FC = ({ if ( selected !== 'tournament' && selected !== 'lichess' && - selected !== 'hb' && - selected !== 'favorites' + selected !== 'hb' ) { const isAlreadyFetched = fetchedCache[selected]?.[currentPage] @@ -215,43 +226,65 @@ export const AnalysisGameList: React.FC = ({ getAnalysisGameList(selected, currentPage) .then((data) => { - let parsedGames + let parsedGames: AnalysisWebGame[] = [] - if (selected === 'custom') { + if (selected === 'favorites') { + // Handle favorites response format parsedGames = data.games.map((game: any) => ({ - id: game.id, - label: game.name || 'Custom Game', - result: '*', - type: game.pgn ? 'custom-pgn' : 'custom-fen', + id: game.game_id || game.id, + type: game.game_type || game.type || 'custom-pgn', + label: game.custom_name || game.label || 'Untitled', + result: game.result || '*', pgn: game.pgn, + is_favorited: true, // All games in favorites are favorited + custom_name: game.custom_name, })) } else { - const parse = ( - game: { - game_id: string - maia_name: string - result: string - player_color: 'white' | 'black' - }, - type: string, - ) => { - const raw = game.maia_name.replace('_kdd_', ' ') - const maia = raw.charAt(0).toUpperCase() + raw.slice(1) - - return { - id: game.game_id, - label: + // Handle regular games response format + + if (selected === 'custom') { + parsedGames = data.games.map((game: any) => ({ + id: game.id, + label: game.name || 'Custom Game', + result: '*', + type: game.pgn ? 'custom-pgn' : 'custom-fen', + pgn: game.pgn, + })) + } else { + const parse = ( + game: { + game_id: string + maia_name: string + result: string + player_color: 'white' | 'black' + is_favorited?: boolean + custom_name?: string + }, + type: string, + ) => { + const raw = game.maia_name.replace('_kdd_', ' ') + const maia = raw.charAt(0).toUpperCase() + raw.slice(1) + + // Use custom name if available, otherwise generate default label + const defaultLabel = game.player_color === 'white' ? `You vs. ${maia}` - : `${maia} vs. You`, - result: game.result, - type, + : `${maia} vs. You` + + return { + id: game.game_id, + label: game.custom_name || defaultLabel, + result: game.result, + type, + is_favorited: game.is_favorited || false, + custom_name: game.custom_name, + } } - } - parsedGames = data.games.map((game: GameData) => - parse(game, selected), - ) + parsedGames = data.games.map((game: GameData) => + parse(game, selected), + ) + } } const calculatedTotalPages = data.total_pages || Math.ceil(data.total_games / 25) @@ -269,6 +302,16 @@ export const AnalysisGameList: React.FC = ({ }, })) + // Update favoritedGameIds from the actual games data + const favoritedIds = new Set( + parsedGames + .filter((game: any) => game.is_favorited) + .map((game: any) => game.id as string), + ) + setFavoritedGameIds( + (prev) => new Set([...prev, ...favoritedIds]), + ) + setLoading(false) }) .catch(() => { @@ -305,20 +348,27 @@ export const AnalysisGameList: React.FC = ({ maia_name: string result: string player_color: 'white' | 'black' + is_favorited?: boolean + custom_name?: string }, type: string, ) => { const raw = game.maia_name.replace('_kdd_', ' ') const maia = raw.charAt(0).toUpperCase() + raw.slice(1) + // Use custom name if available, otherwise generate default label + const defaultLabel = + game.player_color === 'white' + ? `You vs. ${maia}` + : `${maia} vs. You` + return { id: game.game_id, - label: - game.player_color === 'white' - ? `You vs. ${maia}` - : `${maia} vs. You`, + label: game.custom_name || defaultLabel, result: game.result, type, + is_favorited: game.is_favorited || false, + custom_name: game.custom_name, } } @@ -341,6 +391,16 @@ export const AnalysisGameList: React.FC = ({ }, })) + // Update favoritedGameIds from the actual games data + const favoritedIds = new Set( + parsedGames + .filter((game: any) => game.is_favorited) + .map((game: any) => game.id as string), + ) + setFavoritedGameIds( + (prev) => new Set([...prev, ...favoritedIds]), + ) + setLoading(false) }) .catch(() => { @@ -407,17 +467,112 @@ export const AnalysisGameList: React.FC = ({ setFavoriteModal({ isOpen: true, game }) } - const handleSaveFavorite = (customName: string) => { + const handleSaveFavorite = async (customName: string) => { if (favoriteModal.game) { - addFavoriteGame(favoriteModal.game, customName) - setFavoriteGames(getFavoritesAsWebGames()) + await addFavoriteGame(favoriteModal.game, customName) + const updatedFavorites = await getFavoritesAsWebGames() + setFavoriteGames(updatedFavorites) + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + + // Clear favorites cache to force re-fetch + setFetchedCache((prev) => ({ + ...prev, + favorites: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + favorites: {}, + })) + + // Also clear current section cache to show updated favorite status + if (selected !== 'favorites') { + const currentSection = + selected === 'hb' + ? hbSubsection === 'hand' + ? 'hand' + : 'brain' + : selected + setFetchedCache((prev) => ({ + ...prev, + [currentSection]: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + [currentSection]: {}, + })) + } } } - const handleRemoveFavorite = () => { + const handleRemoveFavorite = async () => { if (favoriteModal.game) { - removeFavoriteGame(favoriteModal.game.id) - setFavoriteGames(getFavoritesAsWebGames()) + await removeFavoriteGame(favoriteModal.game.id, favoriteModal.game.type) + const updatedFavorites = await getFavoritesAsWebGames() + setFavoriteGames(updatedFavorites) + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + + // Clear favorites cache to force re-fetch + setFetchedCache((prev) => ({ + ...prev, + favorites: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + favorites: {}, + })) + + // Also clear current section cache to show updated favorite status + if (selected !== 'favorites') { + const currentSection = + selected === 'hb' + ? hbSubsection === 'hand' + ? 'hand' + : 'brain' + : selected + setFetchedCache((prev) => ({ + ...prev, + [currentSection]: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + [currentSection]: {}, + })) + } + } + } + + const handleDirectUnfavorite = async (game: AnalysisWebGame) => { + await removeFavoriteGame(game.id, game.type) + const updatedFavorites = await getFavoritesAsWebGames() + setFavoriteGames(updatedFavorites) + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + + // Clear favorites cache to force re-fetch + setFetchedCache((prev) => ({ + ...prev, + favorites: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + favorites: {}, + })) + + // Also clear current section cache to show updated favorite status + if (selected !== 'favorites') { + const currentSection = + selected === 'hb' + ? hbSubsection === 'hand' + ? 'hand' + : 'brain' + : selected + setFetchedCache((prev) => ({ + ...prev, + [currentSection]: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + [currentSection]: {}, + })) } } @@ -432,11 +587,31 @@ export const AnalysisGameList: React.FC = ({ } else if (selected === 'lichess') { return analysisLichessList } else if (selected === 'favorites') { - return favoriteGames + return gamesByPage.favorites[currentPage] || [] } return [] } + const getModalCurrentName = () => { + if (!favoriteModal.game) return '' + + // If we're in the favorites section, the label is already the custom name + if (selected === 'favorites') { + return favoriteModal.game.label + } + + // For other sections, check if the game is favorited and get its custom name + const favorite = favoriteGames.find( + (fav) => fav.id === favoriteModal.game!.id, + ) + if (favorite) { + return favorite.label // In AnalysisWebGame, the label contains the custom name + } + + // Otherwise, use the game's label + return favoriteModal.game.label + } + return analysisTournamentList ? (
= ({ <> {getCurrentGames().map((game, index) => { const selectedGame = currentId && currentId[0] === game.id - const isFavorited = isFavoriteGame(game.id) + const isFavorited = (game as any).is_favorited || false + const displayName = game.label // This now contains the custom name if favorited return (
= ({ className={`flex h-full w-9 items-center justify-center ${selectedGame ? 'bg-background-3' : 'bg-background-2 group-hover:bg-white/5'}`} >

- {selected === 'play' || selected === 'hb' + {selected === 'play' || + selected === 'hb' || + selected === 'favorites' ? (currentPage - 1) * 25 + index + 1 : index + 1}

@@ -589,7 +767,7 @@ export const AnalysisGameList: React.FC = ({ >

- {game.label} + {displayName}

{selected === 'favorites' && (game.type === 'hand' || @@ -603,18 +781,32 @@ export const AnalysisGameList: React.FC = ({
{selected === 'favorites' && ( - + <> + + + )} {selected !== 'favorites' && (
) })} - {(selected === 'play' || selected === 'hb') && + {(selected === 'play' || + selected === 'hb' || + selected === 'favorites') && totalPages > 1 && (
setFavoriteModal({ isOpen: false, game: null })} onSave={handleSaveFavorite} onRemove={ - favoriteModal.game && isFavoriteGame(favoriteModal.game.id) + favoriteModal.game && favoritedGameIds.has(favoriteModal.game.id) ? handleRemoveFavorite : undefined } diff --git a/src/components/Common/FavoriteModal.tsx b/src/components/Common/FavoriteModal.tsx index b59d6ca3..8c7d9393 100644 --- a/src/components/Common/FavoriteModal.tsx +++ b/src/components/Common/FavoriteModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' interface FavoriteModalProps { isOpen: boolean @@ -17,6 +17,13 @@ export const FavoriteModal: React.FC = ({ }) => { const [name, setName] = useState(currentName) + // Reset the name when modal opens with a new currentName + useEffect(() => { + if (isOpen) { + setName(currentName) + } + }, [isOpen, currentName]) + if (!isOpen) return null const handleSave = () => { diff --git a/src/components/Common/Header.tsx b/src/components/Common/Header.tsx index 48ec4d26..4905fef3 100644 --- a/src/components/Common/Header.tsx +++ b/src/components/Common/Header.tsx @@ -378,7 +378,7 @@ export const Header: React.FC = () => { Settings - diff --git a/src/components/Profile/GameList.tsx b/src/components/Profile/GameList.tsx index bda3bbc0..a84523ab 100644 --- a/src/components/Profile/GameList.tsx +++ b/src/components/Profile/GameList.tsx @@ -52,6 +52,7 @@ export const GameList = ({ play: {}, hand: {}, brain: {}, + favorites: {}, }) const [customAnalyses, setCustomAnalyses] = useState(() => { @@ -60,12 +61,10 @@ export const GameList = ({ } return [] }) - const [favoriteGames, setFavoriteGames] = useState(() => { - if (typeof window !== 'undefined') { - return getFavoritesAsWebGames() - } - return [] - }) + const [favoriteGames, setFavoriteGames] = useState([]) + const [favoritedGameIds, setFavoritedGameIds] = useState>( + new Set(), + ) const [currentPage, setCurrentPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [loading, setLoading] = useState(false) @@ -83,6 +82,7 @@ export const GameList = ({ hand: {}, brain: {}, lichess: {}, + favorites: {}, }) const [totalPagesCache, setTotalPagesCache] = useState<{ @@ -96,6 +96,7 @@ export const GameList = ({ hand: 1, brain: 1, lichess: 1, + favorites: 1, }) // Update custom analyses and favorites when component mounts @@ -103,7 +104,16 @@ export const GameList = ({ if (showCustom) { setCustomAnalyses(getCustomAnalysesAsWebGames()) } - setFavoriteGames(getFavoritesAsWebGames()) + // Load favorites (supports both sync and async implementations) + Promise.resolve(getFavoritesAsWebGames()) + .then((favorites) => { + setFavoriteGames(favorites) + setFavoritedGameIds(new Set(favorites.map((f) => f.id))) + }) + .catch(() => { + setFavoriteGames([]) + setFavoritedGameIds(new Set()) + }) }, []) useEffect(() => { @@ -131,12 +141,7 @@ export const GameList = ({ useEffect(() => { const targetUser = lichessId || user?.lichessId - if ( - targetUser && - selected !== 'lichess' && - selected !== 'custom' && - selected !== 'favorites' - ) { + if (targetUser && selected !== 'lichess' && selected !== 'custom') { const gameType = selected === 'hb' ? hbSubsection : selected const isAlreadyFetched = fetchedCache[gameType]?.[currentPage] @@ -150,34 +155,57 @@ export const GameList = ({ getAnalysisGameList(gameType, currentPage, lichessId) .then((data) => { - const parse = ( - game: { - game_id: string - maia_name: string - result: string - player_color: 'white' | 'black' - }, - type: string, - ) => { - const raw = game.maia_name.replace('_kdd_', ' ') - const maia = raw.charAt(0).toUpperCase() + raw.slice(1) - - const playerLabel = userName || 'You' - - return { - id: game.game_id, - label: + let parsedGames: AnalysisWebGame[] = [] + + if (gameType === 'favorites') { + // Handle favorites response format + parsedGames = data.games.map((game: any) => ({ + id: game.game_id || game.id, + type: game.game_type || game.type || 'custom-pgn', + label: game.custom_name || game.label || 'Untitled', + result: game.result || '*', + pgn: game.pgn, + is_favorited: true, // All games in favorites are favorited + custom_name: game.custom_name, + })) + } else { + // Handle regular games response format + const parse = ( + game: { + game_id: string + maia_name: string + result: string + player_color: 'white' | 'black' + is_favorited?: boolean + custom_name?: string + }, + type: string, + ) => { + const raw = game.maia_name.replace('_kdd_', ' ') + const maia = raw.charAt(0).toUpperCase() + raw.slice(1) + + const playerLabel = userName || 'You' + + // Use custom name if available, otherwise generate default label + const defaultLabel = game.player_color === 'white' ? `${playerLabel} vs. ${maia}` - : `${maia} vs. ${playerLabel}`, - result: game.result, - type, + : `${maia} vs. ${playerLabel}` + + return { + id: game.game_id, + label: game.custom_name || defaultLabel, + result: game.result, + type, + is_favorited: game.is_favorited || false, + custom_name: game.custom_name, + } } - } - const parsedGames = data.games.map((game: GameData) => - parse(game, gameType), - ) + parsedGames = data.games.map((game: GameData) => + parse(game, gameType), + ) + } const calculatedTotalPages = data.total_pages || Math.ceil(data.total_games / 25) @@ -195,6 +223,14 @@ export const GameList = ({ }, })) + // Update favoritedGameIds from the actual games data + const favoritedIds = new Set( + parsedGames + .filter((game: any) => game.is_favorited) + .map((game: any) => game.id), + ) + setFavoritedGameIds((prev) => new Set([...prev, ...favoritedIds])) + setLoading(false) }) .catch(() => { @@ -277,17 +313,97 @@ export const GameList = ({ setFavoriteModal({ isOpen: true, game }) } - const handleSaveFavorite = (customName: string) => { + const handleSaveFavorite = async (customName: string) => { if (favoriteModal.game) { - addFavoriteGame(favoriteModal.game, customName) - setFavoriteGames(getFavoritesAsWebGames()) + await addFavoriteGame(favoriteModal.game, customName) + const updatedFavorites = await getFavoritesAsWebGames() + setFavoriteGames(updatedFavorites) + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + + // Clear favorites cache to force re-fetch + setFetchedCache((prev) => ({ + ...prev, + favorites: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + favorites: {}, + })) + + // Also clear current section cache to show updated favorite status + if (selected !== 'favorites') { + const currentSection = selected === 'hb' ? hbSubsection : selected + setFetchedCache((prev) => ({ + ...prev, + [currentSection]: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + [currentSection]: {}, + })) + } } } - const handleRemoveFavorite = () => { + const handleRemoveFavorite = async () => { if (favoriteModal.game) { - removeFavoriteGame(favoriteModal.game.id) - setFavoriteGames(getFavoritesAsWebGames()) + await removeFavoriteGame(favoriteModal.game.id, favoriteModal.game.type) + const updatedFavorites = await getFavoritesAsWebGames() + setFavoriteGames(updatedFavorites) + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + + // Clear favorites cache to force re-fetch + setFetchedCache((prev) => ({ + ...prev, + favorites: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + favorites: {}, + })) + + // Also clear current section cache to show updated favorite status + if (selected !== 'favorites') { + const currentSection = selected === 'hb' ? hbSubsection : selected + setFetchedCache((prev) => ({ + ...prev, + [currentSection]: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + [currentSection]: {}, + })) + } + } + } + + const handleDirectUnfavorite = async (game: AnalysisWebGame) => { + await removeFavoriteGame(game.id, game.type) + const updatedFavorites = await getFavoritesAsWebGames() + setFavoriteGames(updatedFavorites) + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + + // Clear favorites cache to force re-fetch + setFetchedCache((prev) => ({ + ...prev, + favorites: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + favorites: {}, + })) + + // Also clear current section cache to show updated favorite status + if (selected !== 'favorites') { + const currentSection = selected === 'hb' ? hbSubsection : selected + setFetchedCache((prev) => ({ + ...prev, + [currentSection]: {}, + })) + setGamesByPage((prev) => ({ + ...prev, + [currentSection]: {}, + })) } } @@ -302,11 +418,31 @@ export const GameList = ({ } else if (selected === 'lichess' && showLichess) { return games } else if (selected === 'favorites') { - return favoriteGames + return gamesByPage.favorites[currentPage] || [] } return [] } + const getModalCurrentName = () => { + if (!favoriteModal.game) return '' + + // If we're in the favorites section, the label is already the custom name + if (selected === 'favorites') { + return favoriteModal.game.label + } + + // For other sections, check if the game is favorited and get its custom name + const favorite = favoriteGames.find( + (fav) => fav.id === favoriteModal.game!.id, + ) + if (favorite) { + return favorite.label // In AnalysisWebGame, the label contains the custom name + } + + // Otherwise, use the game's label + return favoriteModal.game.label + } + return (
@@ -409,7 +545,8 @@ export const GameList = ({ ) : ( <> {getCurrentGames().map((game, index) => { - const isFavorited = isFavoriteGame(game.id) + const isFavorited = (game as any).is_favorited || false + const displayName = game.label // This now contains the custom name if favorited return (

- {selected === 'play' || selected === 'hb' + {selected === 'play' || + selected === 'hb' || + selected === 'favorites' ? (currentPage - 1) * 25 + index + 1 : index + 1}

@@ -432,7 +571,7 @@ export const GameList = ({ >

- {game.label} + {displayName}

{selected === 'favorites' && (game.type === 'hand' || game.type === 'brain') && ( @@ -493,48 +632,51 @@ export const GameList = ({
{/* Pagination */} - {(selected === 'play' || selected === 'hb') && totalPages > 1 && ( -
- - - - Page {currentPage} of {totalPages} - - - -
- )} + {(selected === 'play' || selected === 'hb' || selected === 'favorites') && + totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} setFavoriteModal({ isOpen: false, game: null })} onSave={handleSaveFavorite} onRemove={ - favoriteModal.game && isFavoriteGame(favoriteModal.game.id) + favoriteModal.game && favoritedGameIds.has(favoriteModal.game.id) ? handleRemoveFavorite : undefined } diff --git a/src/lib/customAnalysis.ts b/src/lib/customAnalysis.ts index ef96750c..00ae70b5 100644 --- a/src/lib/customAnalysis.ts +++ b/src/lib/customAnalysis.ts @@ -55,10 +55,10 @@ export const saveCustomAnalysis = async ( return analysis } catch (error) { console.error('Failed to store custom game on backend:', error) - + const analyses = getLocalStoredCustomAnalyses() const id = `${type}-${Date.now()}` - + const analysis: StoredCustomAnalysis = { id, name: finalName, @@ -88,7 +88,7 @@ const getLocalStoredCustomAnalyses = (): StoredCustomAnalysis[] => { const migrateLocalStorageToBackend = async (): Promise => { if (typeof window === 'undefined') return - + const hasBeenMigrated = localStorage.getItem(MIGRATION_KEY) if (hasBeenMigrated) return @@ -115,13 +115,15 @@ const migrateLocalStorageToBackend = async (): Promise => { } } - const successCount = migrationResults.filter(r => r.success).length - console.log(`Migration completed: ${successCount}/${localAnalyses.length} analyses migrated successfully`) + const successCount = migrationResults.filter((r) => r.success).length + console.log( + `Migration completed: ${successCount}/${localAnalyses.length} analyses migrated successfully`, + ) if (successCount === localAnalyses.length) { localStorage.removeItem(STORAGE_KEY) } - + localStorage.setItem(MIGRATION_KEY, 'true') } diff --git a/src/lib/favorites.ts b/src/lib/favorites.ts index 85bccb15..3a05c4a1 100644 --- a/src/lib/favorites.ts +++ b/src/lib/favorites.ts @@ -1,4 +1,8 @@ import { AnalysisWebGame } from 'src/types' +import { + updateGameMetadata, + getAnalysisGameList, +} from 'src/api/analysis/analysis' export interface FavoriteGame { id: string @@ -12,22 +16,38 @@ export interface FavoriteGame { const STORAGE_KEY = 'maia_favorite_games' -export const addFavoriteGame = ( +const mapGameTypeToApiType = ( + gameType: AnalysisWebGame['type'], +): 'custom' | 'play' | 'hand' | 'brain' => { + switch (gameType) { + case 'custom-pgn': + case 'custom-fen': + return 'custom' + case 'play': + return 'play' + case 'hand': + return 'hand' + case 'brain': + return 'brain' + default: + // Default to 'custom' for other types like 'tournament', 'pgn', 'stream' + return 'custom' + } +} + +export const addFavoriteGame = async ( game: AnalysisWebGame, customName?: string, -): FavoriteGame => { - const favorites = getFavoriteGames() - - // Check if already favorited - const existingIndex = favorites.findIndex((fav) => fav.id === game.id) - if (existingIndex !== -1) { - // Update existing favorite - favorites[existingIndex] = { - ...favorites[existingIndex], - customName: customName || favorites[existingIndex].customName, - } - } else { - // Add new favorite +): Promise => { + try { + // First try to update via API + const gameType = mapGameTypeToApiType(game.type) + await updateGameMetadata(gameType, game.id, { + is_favorited: true, + custom_name: customName || game.label, + }) + + // Create the FavoriteGame object for return value const favorite: FavoriteGame = { id: game.id, type: game.type, @@ -37,36 +57,136 @@ export const addFavoriteGame = ( addedAt: new Date().toISOString(), pgn: game.pgn, } - favorites.unshift(favorite) - } - // Limit to 100 favorites - const trimmedFavorites = favorites.slice(0, 100) - localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmedFavorites)) + return favorite + } catch (error) { + console.warn( + 'Failed to favorite via API, falling back to localStorage:', + error, + ) + + // Fallback to localStorage + const favorites = getFavoriteGamesFromStorage() + + // Check if already favorited + const existingIndex = favorites.findIndex((fav) => fav.id === game.id) + if (existingIndex !== -1) { + // Update existing favorite + favorites[existingIndex] = { + ...favorites[existingIndex], + customName: customName || favorites[existingIndex].customName, + } + } else { + // Add new favorite + const favorite: FavoriteGame = { + id: game.id, + type: game.type, + originalLabel: game.label, + customName: customName || game.label, + result: game.result, + addedAt: new Date().toISOString(), + pgn: game.pgn, + } + favorites.unshift(favorite) + } + + // Limit to 100 favorites + const trimmedFavorites = favorites.slice(0, 100) + localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmedFavorites)) - return favorites[existingIndex] || favorites[0] + return favorites[existingIndex] || favorites[0] + } } -export const removeFavoriteGame = (gameId: string): void => { - const favorites = getFavoriteGames() - const filtered = favorites.filter((favorite) => favorite.id !== gameId) - localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)) +export const removeFavoriteGame = async ( + gameId: string, + gameType?: AnalysisWebGame['type'], +): Promise => { + try { + // First try to update via API if game type is provided + if (gameType) { + const apiGameType = mapGameTypeToApiType(gameType) + await updateGameMetadata(apiGameType, gameId, { + is_favorited: false, + }) + return + } + + // If no game type provided, try to find it in localStorage first + const localFavorites = getFavoriteGamesFromStorage() + const existingFavorite = localFavorites.find((fav) => fav.id === gameId) + + if (existingFavorite) { + const apiGameType = mapGameTypeToApiType(existingFavorite.type) + await updateGameMetadata(apiGameType, gameId, { + is_favorited: false, + }) + return + } + + // If not found in localStorage, we can't determine the game type for API + throw new Error('Game type required for API call') + } catch (error) { + console.warn( + 'Failed to unfavorite via API, falling back to localStorage:', + error, + ) + + // Fallback to localStorage + const favorites = getFavoriteGamesFromStorage() + const filtered = favorites.filter((favorite) => favorite.id !== gameId) + localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)) + } } -export const updateFavoriteName = ( +export const updateFavoriteName = async ( gameId: string, customName: string, -): void => { - const favorites = getFavoriteGames() - const favoriteIndex = favorites.findIndex((fav) => fav.id === gameId) + gameType?: AnalysisWebGame['type'], +): Promise => { + try { + // First try to update via API if game type is provided + if (gameType) { + const apiGameType = mapGameTypeToApiType(gameType) + await updateGameMetadata(apiGameType, gameId, { + custom_name: customName, + }) + return + } + + // If no game type provided, try to find it in localStorage first + const localFavorites = getFavoriteGamesFromStorage() + const existingFavorite = localFavorites.find((fav) => fav.id === gameId) + + if (existingFavorite) { + const apiGameType = mapGameTypeToApiType(existingFavorite.type) + await updateGameMetadata(apiGameType, gameId, { + custom_name: customName, + }) + return + } - if (favoriteIndex !== -1) { - favorites[favoriteIndex].customName = customName - localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites)) + // If not found in localStorage, we can't determine the game type for API + throw new Error('Game type required for API call') + } catch (error) { + console.warn( + 'Failed to update name via API, falling back to localStorage:', + error, + ) + + // Fallback to localStorage + const favorites = getFavoriteGamesFromStorage() + const favoriteIndex = favorites.findIndex((fav) => fav.id === gameId) + + if (favoriteIndex !== -1) { + favorites[favoriteIndex].customName = customName + localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites)) + } } } -export const getFavoriteGames = (): FavoriteGame[] => { +// Helper function to get favorites from localStorage only +const getFavoriteGamesFromStorage = (): FavoriteGame[] => { try { const stored = localStorage.getItem(STORAGE_KEY) return stored ? JSON.parse(stored) : [] @@ -76,13 +196,48 @@ export const getFavoriteGames = (): FavoriteGame[] => { } } -export const isFavoriteGame = (gameId: string): boolean => { - const favorites = getFavoriteGames() +export const getFavoriteGames = async (): Promise => { + try { + // Fetch favorites using the special "favorites" game type endpoint + const response = await getAnalysisGameList('favorites', 1) + + // Convert API response to FavoriteGame format + if (response.games && Array.isArray(response.games)) { + const favorites = response.games.map( + (game: any) => + ({ + id: game.id, + type: game.game_type || game.type || 'custom-pgn', // Use the game_type field from API + originalLabel: game.label || game.custom_name || 'Untitled', + customName: game.custom_name || game.label || 'Untitled', + result: game.result || '*', + addedAt: game.created_at || new Date().toISOString(), + pgn: game.pgn, + }) as FavoriteGame, + ) + + return favorites + } + + return [] + } catch (error) { + console.warn( + 'Failed to fetch favorites from API, falling back to localStorage:', + error, + ) + return getFavoriteGamesFromStorage() + } +} + +export const isFavoriteGame = async (gameId: string): Promise => { + const favorites = await getFavoriteGames() return favorites.some((favorite) => favorite.id === gameId) } -export const getFavoriteGame = (gameId: string): FavoriteGame | undefined => { - const favorites = getFavoriteGames() +export const getFavoriteGame = async ( + gameId: string, +): Promise => { + const favorites = await getFavoriteGames() return favorites.find((favorite) => favorite.id === gameId) } @@ -98,7 +253,7 @@ export const convertFavoriteToWebGame = ( } } -export const getFavoritesAsWebGames = (): AnalysisWebGame[] => { - const favorites = getFavoriteGames() +export const getFavoritesAsWebGames = async (): Promise => { + const favorites = await getFavoriteGames() return favorites.map(convertFavoriteToWebGame) }