From 50b557527599411bd4f58ebdceea74a2d912379f Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Fri, 8 Aug 2025 13:00:44 -0400 Subject: [PATCH 1/4] feat: add initial migration to backend storage for favourited games --- __tests__/lib/favorites.test.ts | 62 +++--- src/api/analysis/analysis.ts | 35 +++ src/components/Analysis/AnalysisGameList.tsx | 37 ++-- src/components/Profile/GameList.tsx | 37 ++-- src/lib/favorites.ts | 220 +++++++++++++++---- 5 files changed, 295 insertions(+), 96 deletions(-) diff --git a/__tests__/lib/favorites.test.ts b/__tests__/lib/favorites.test.ts index 4b56a3ff..325f740c 100644 --- a/__tests__/lib/favorites.test.ts +++ b/__tests__/lib/favorites.test.ts @@ -31,6 +31,12 @@ 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 +50,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 +117,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 +126,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/src/api/analysis/analysis.ts b/src/api/analysis/analysis.ts index 16ea177e..731829f8 100644 --- a/src/api/analysis/analysis.ts +++ b/src/api/analysis/analysis.ts @@ -80,6 +80,7 @@ export const getAnalysisGameList = async ( type = 'play', page = 1, lichessId?: string, + favoritesOnly?: boolean, ) => { const url = buildUrl(`analysis/user/list/${type}/${page}`) const searchParams = new URLSearchParams() @@ -88,6 +89,10 @@ export const getAnalysisGameList = async ( searchParams.append('lichess_id', lichessId) } + if (favoritesOnly !== undefined) { + searchParams.append('favorites_only', String(favoritesOnly)) + } + const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url @@ -718,3 +723,33 @@ 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') + } +} diff --git a/src/components/Analysis/AnalysisGameList.tsx b/src/components/Analysis/AnalysisGameList.tsx index 5c9007ba..53f4d1d3 100644 --- a/src/components/Analysis/AnalysisGameList.tsx +++ b/src/components/Analysis/AnalysisGameList.tsx @@ -92,12 +92,8 @@ export const AnalysisGameList: React.FC = ({ } return [] }) - 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 @@ -108,7 +104,14 @@ export const AnalysisGameList: React.FC = ({ useEffect(() => { setCustomAnalyses(getCustomAnalysesAsWebGames()) - 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(() => { @@ -386,17 +389,21 @@ 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))) } } - 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))) } } @@ -533,7 +540,7 @@ export const AnalysisGameList: React.FC = ({ <> {getCurrentGames().map((game, index) => { const selectedGame = currentId && currentId[0] === game.id - const isFavorited = isFavoriteGame(game.id) + const isFavorited = favoritedGameIds.has(game.id) return (
= ({ onClose={() => 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/Profile/GameList.tsx b/src/components/Profile/GameList.tsx index bda3bbc0..c43ff448 100644 --- a/src/components/Profile/GameList.tsx +++ b/src/components/Profile/GameList.tsx @@ -60,12 +60,8 @@ 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) @@ -103,7 +99,14 @@ export const GameList = ({ if (showCustom) { setCustomAnalyses(getCustomAnalysesAsWebGames()) } - setFavoriteGames(getFavoritesAsWebGames()) + // Load favorites asynchronously + getFavoritesAsWebGames().then((favorites) => { + setFavoriteGames(favorites) + setFavoritedGameIds(new Set(favorites.map(f => f.id))) + }).catch(() => { + setFavoriteGames([]) + setFavoritedGameIds(new Set()) + }) }, []) useEffect(() => { @@ -277,17 +280,21 @@ 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))) } } - 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))) } } @@ -409,7 +416,7 @@ export const GameList = ({ ) : ( <> {getCurrentGames().map((game, index) => { - const isFavorited = isFavoriteGame(game.id) + const isFavorited = favoritedGameIds.has(game.id) return (
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/favorites.ts b/src/lib/favorites.ts index 85bccb15..917f3a47 100644 --- a/src/lib/favorites.ts +++ b/src/lib/favorites.ts @@ -1,4 +1,5 @@ import { AnalysisWebGame } from 'src/types' +import { updateGameMetadata, getAnalysisGameList } from 'src/api/analysis/analysis' export interface FavoriteGame { id: string @@ -12,22 +13,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 +54,127 @@ 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() - return favorites[existingIndex] || favorites[0] + // 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] + } } -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 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)) + 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 +184,49 @@ export const getFavoriteGames = (): FavoriteGame[] => { } } -export const isFavoriteGame = (gameId: string): boolean => { - const favorites = getFavoriteGames() +export const getFavoriteGames = async (): Promise => { + try { + // Try to fetch from API for each game type + const gameTypes: Array<'custom' | 'play' | 'hand' | 'brain'> = ['custom', 'play', 'hand', 'brain'] + const allFavorites: FavoriteGame[] = [] + + for (const gameType of gameTypes) { + try { + const response = await getAnalysisGameList(gameType, 1, undefined, true) + + // Convert API response to FavoriteGame format + if (response.games && Array.isArray(response.games)) { + const favorites = response.games.map((game: any) => ({ + id: game.id, + type: gameType === 'custom' ? 'custom-pgn' : gameType, // Default custom to custom-pgn + 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)) + + allFavorites.push(...favorites) + } + } catch (typeError) { + console.warn(`Failed to fetch favorites for ${gameType}:`, typeError) + } + } + + return allFavorites + } 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 +242,7 @@ export const convertFavoriteToWebGame = ( } } -export const getFavoritesAsWebGames = (): AnalysisWebGame[] => { - const favorites = getFavoriteGames() +export const getFavoritesAsWebGames = async (): Promise => { + const favorites = await getFavoriteGames() return favorites.map(convertFavoriteToWebGame) } From 48ad0175d0c0eb1559adddaf4ff56a757b36d6d2 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Sat, 9 Aug 2025 18:23:21 -0400 Subject: [PATCH 2/4] feat: handle edge-cases with favourite game logic --- src/api/analysis/analysis.ts | 5 - src/components/Analysis/AnalysisGameList.tsx | 253 +++++++++++++++---- src/components/Common/FavoriteModal.tsx | 9 +- src/components/Profile/GameList.tsx | 195 +++++++++++--- src/lib/favorites.ts | 41 ++- 5 files changed, 390 insertions(+), 113 deletions(-) diff --git a/src/api/analysis/analysis.ts b/src/api/analysis/analysis.ts index 731829f8..d87a27a3 100644 --- a/src/api/analysis/analysis.ts +++ b/src/api/analysis/analysis.ts @@ -80,7 +80,6 @@ export const getAnalysisGameList = async ( type = 'play', page = 1, lichessId?: string, - favoritesOnly?: boolean, ) => { const url = buildUrl(`analysis/user/list/${type}/${page}`) const searchParams = new URLSearchParams() @@ -89,10 +88,6 @@ export const getAnalysisGameList = async ( searchParams.append('lichess_id', lichessId) } - if (favoritesOnly !== undefined) { - searchParams.append('favorites_only', String(favoritesOnly)) - } - const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url diff --git a/src/components/Analysis/AnalysisGameList.tsx b/src/components/Analysis/AnalysisGameList.tsx index 53f4d1d3..9043db3a 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,6 +86,7 @@ export const AnalysisGameList: React.FC = ({ play: {}, hand: {}, brain: {}, + favorites: {}, }) const [customAnalyses, setCustomAnalyses] = useState(() => { @@ -128,6 +131,7 @@ export const AnalysisGameList: React.FC = ({ brain: {}, lichess: {}, tournament: {}, + favorites: {}, }) const [totalPagesCache, setTotalPagesCache] = useState<{ @@ -142,6 +146,7 @@ export const AnalysisGameList: React.FC = ({ brain: 1, lichess: 1, tournament: 1, + favorites: 1, }) const listKeys = useMemo(() => { @@ -193,8 +198,7 @@ export const AnalysisGameList: React.FC = ({ selected !== 'tournament' && selected !== 'lichess' && selected !== 'custom' && - selected !== 'hb' && - selected !== 'favorites' + selected !== 'hb' ) { const isAlreadyFetched = fetchedCache[selected]?.[currentPage] @@ -208,32 +212,54 @@ export const AnalysisGameList: React.FC = ({ getAnalysisGameList(selected, currentPage) .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) - - return { - id: game.game_id, - label: - game.player_color === 'white' - ? `You vs. ${maia}` - : `${maia} vs. You`, - result: game.result, - type, + let parsedGames: AnalysisWebGame[] = [] + + if (selected === '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) + + // 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.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, selected), - ) + parsedGames = data.games.map((game: GameData) => + parse(game, selected), + ) + } const calculatedTotalPages = data.total_pages || Math.ceil(data.total_games / 25) @@ -249,6 +275,14 @@ export const AnalysisGameList: React.FC = ({ [currentPage]: parsedGames, }, })) + + // 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) }) @@ -286,20 +320,26 @@ 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, } } @@ -321,6 +361,14 @@ export const AnalysisGameList: React.FC = ({ [currentPage]: parsedGames, }, })) + + // 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) }) @@ -395,6 +443,29 @@ export const AnalysisGameList: React.FC = ({ 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]: {}, + })) + } } } @@ -404,6 +475,59 @@ export const AnalysisGameList: React.FC = ({ 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]: {}, + })) } } @@ -418,11 +542,29 @@ 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 = favoritedGameIds.has(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}

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

- {game.label} + {displayName}

{selected === 'favorites' && (game.type === 'hand' || @@ -589,18 +732,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={ 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/Profile/GameList.tsx b/src/components/Profile/GameList.tsx index c43ff448..f7e7b59e 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(() => { @@ -79,6 +80,7 @@ export const GameList = ({ hand: {}, brain: {}, lichess: {}, + favorites: {}, }) const [totalPagesCache, setTotalPagesCache] = useState<{ @@ -92,6 +94,7 @@ export const GameList = ({ hand: 1, brain: 1, lichess: 1, + favorites: 1, }) // Update custom analyses and favorites when component mounts @@ -137,8 +140,7 @@ export const GameList = ({ if ( targetUser && selected !== 'lichess' && - selected !== 'custom' && - selected !== 'favorites' + selected !== 'custom' ) { const gameType = selected === 'hb' ? hbSubsection : selected const isAlreadyFetched = fetchedCache[gameType]?.[currentPage] @@ -153,34 +155,56 @@ 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: - game.player_color === 'white' - ? `${playerLabel} vs. ${maia}` - : `${maia} vs. ${playerLabel}`, - result: game.result, - type, + 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}` + + 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) @@ -197,6 +221,14 @@ export const GameList = ({ [currentPage]: parsedGames, }, })) + + // 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) }) @@ -286,6 +318,29 @@ export const GameList = ({ 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]: {}, + })) + } } } @@ -295,6 +350,59 @@ export const GameList = ({ 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]: {}, + })) } } @@ -309,11 +417,29 @@ 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 (
@@ -416,7 +542,8 @@ export const GameList = ({ ) : ( <> {getCurrentGames().map((game, index) => { - const isFavorited = favoritedGameIds.has(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}

@@ -439,7 +566,7 @@ export const GameList = ({ >

- {game.label} + {displayName}

{selected === 'favorites' && (game.type === 'hand' || game.type === 'brain') && ( @@ -500,7 +627,7 @@ export const GameList = ({
{/* Pagination */} - {(selected === 'play' || selected === 'hb') && totalPages > 1 && ( + {(selected === 'play' || selected === 'hb' || selected === 'favorites') && totalPages > 1 && (
) })} - {(selected === 'play' || selected === 'hb' || selected === 'favorites') && + {(selected === 'play' || + selected === 'hb' || + selected === 'favorites') && totalPages > 1 && (
diff --git a/src/components/Profile/GameList.tsx b/src/components/Profile/GameList.tsx index f7e7b59e..b3e62f12 100644 --- a/src/components/Profile/GameList.tsx +++ b/src/components/Profile/GameList.tsx @@ -62,7 +62,9 @@ export const GameList = ({ return [] }) const [favoriteGames, setFavoriteGames] = useState([]) - const [favoritedGameIds, setFavoritedGameIds] = useState>(new Set()) + const [favoritedGameIds, setFavoritedGameIds] = useState>( + new Set(), + ) const [currentPage, setCurrentPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [loading, setLoading] = useState(false) @@ -103,13 +105,15 @@ export const GameList = ({ setCustomAnalyses(getCustomAnalysesAsWebGames()) } // Load favorites asynchronously - getFavoritesAsWebGames().then((favorites) => { - setFavoriteGames(favorites) - setFavoritedGameIds(new Set(favorites.map(f => f.id))) - }).catch(() => { - setFavoriteGames([]) - setFavoritedGameIds(new Set()) - }) + getFavoritesAsWebGames() + .then((favorites) => { + setFavoriteGames(favorites) + setFavoritedGameIds(new Set(favorites.map((f) => f.id))) + }) + .catch(() => { + setFavoriteGames([]) + setFavoritedGameIds(new Set()) + }) }, []) useEffect(() => { @@ -137,11 +141,7 @@ export const GameList = ({ useEffect(() => { const targetUser = lichessId || user?.lichessId - if ( - targetUser && - selected !== 'lichess' && - selected !== 'custom' - ) { + if (targetUser && selected !== 'lichess' && selected !== 'custom') { const gameType = selected === 'hb' ? hbSubsection : selected const isAlreadyFetched = fetchedCache[gameType]?.[currentPage] @@ -156,7 +156,7 @@ export const GameList = ({ getAnalysisGameList(gameType, currentPage, lichessId) .then((data) => { let parsedGames: AnalysisWebGame[] = [] - + if (gameType === 'favorites') { // Handle favorites response format parsedGames = data.games.map((game: any) => ({ @@ -185,11 +185,12 @@ export const GameList = ({ 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}` + const defaultLabel = + game.player_color === 'white' + ? `${playerLabel} vs. ${maia}` + : `${maia} vs. ${playerLabel}` return { id: game.game_id, @@ -221,12 +222,12 @@ export const GameList = ({ [currentPage]: parsedGames, }, })) - + // Update favoritedGameIds from the actual games data const favoritedIds = new Set( parsedGames .filter((game: any) => game.is_favorited) - .map((game: any) => game.id) + .map((game: any) => game.id), ) setFavoritedGameIds((prev) => new Set([...prev, ...favoritedIds])) @@ -317,8 +318,8 @@ export const GameList = ({ await addFavoriteGame(favoriteModal.game, customName) const updatedFavorites = await getFavoritesAsWebGames() setFavoriteGames(updatedFavorites) - setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id))) - + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + // Clear favorites cache to force re-fetch setFetchedCache((prev) => ({ ...prev, @@ -328,7 +329,7 @@ export const GameList = ({ ...prev, favorites: {}, })) - + // Also clear current section cache to show updated favorite status if (selected !== 'favorites') { const currentSection = selected === 'hb' ? hbSubsection : selected @@ -349,8 +350,8 @@ export const GameList = ({ await removeFavoriteGame(favoriteModal.game.id, favoriteModal.game.type) const updatedFavorites = await getFavoritesAsWebGames() setFavoriteGames(updatedFavorites) - setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id))) - + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + // Clear favorites cache to force re-fetch setFetchedCache((prev) => ({ ...prev, @@ -360,7 +361,7 @@ export const GameList = ({ ...prev, favorites: {}, })) - + // Also clear current section cache to show updated favorite status if (selected !== 'favorites') { const currentSection = selected === 'hb' ? hbSubsection : selected @@ -380,8 +381,8 @@ export const GameList = ({ await removeFavoriteGame(game.id, game.type) const updatedFavorites = await getFavoritesAsWebGames() setFavoriteGames(updatedFavorites) - setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id))) - + setFavoritedGameIds(new Set(updatedFavorites.map((f) => f.id))) + // Clear favorites cache to force re-fetch setFetchedCache((prev) => ({ ...prev, @@ -391,7 +392,7 @@ export const GameList = ({ ...prev, favorites: {}, })) - + // Also clear current section cache to show updated favorite status if (selected !== 'favorites') { const currentSection = selected === 'hb' ? hbSubsection : selected @@ -424,18 +425,20 @@ export const GameList = ({ 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) + 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 } @@ -555,7 +558,9 @@ export const GameList = ({ >

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

@@ -627,41 +632,44 @@ export const GameList = ({
{/* Pagination */} - {(selected === 'play' || selected === 'hb' || selected === 'favorites') && totalPages > 1 && ( -
- - - - Page {currentPage} of {totalPages} - - - -
- )} + {(selected === 'play' || selected === 'hb' || selected === 'favorites') && + totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} { 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 2ad68d32..3a05c4a1 100644 --- a/src/lib/favorites.ts +++ b/src/lib/favorites.ts @@ -1,5 +1,8 @@ import { AnalysisWebGame } from 'src/types' -import { updateGameMetadata, getAnalysisGameList } from 'src/api/analysis/analysis' +import { + updateGameMetadata, + getAnalysisGameList, +} from 'src/api/analysis/analysis' export interface FavoriteGame { id: string @@ -57,8 +60,11 @@ export const addFavoriteGame = async ( return favorite } catch (error) { - console.warn('Failed to favorite via API, falling back to localStorage:', error) - + console.warn( + 'Failed to favorite via API, falling back to localStorage:', + error, + ) + // Fallback to localStorage const favorites = getFavoriteGamesFromStorage() @@ -105,11 +111,11 @@ export const removeFavoriteGame = async ( }) 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, { @@ -117,12 +123,15 @@ export const removeFavoriteGame = async ( }) 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) - + 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) @@ -144,11 +153,11 @@ export const updateFavoriteName = async ( }) 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, { @@ -156,12 +165,15 @@ export const updateFavoriteName = async ( }) 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 update name via API, falling back to localStorage:', 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) @@ -188,25 +200,31 @@ 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)) - + 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) + console.warn( + 'Failed to fetch favorites from API, falling back to localStorage:', + error, + ) return getFavoriteGamesFromStorage() } } @@ -216,7 +234,9 @@ export const isFavoriteGame = async (gameId: string): Promise => { return favorites.some((favorite) => favorite.id === gameId) } -export const getFavoriteGame = async (gameId: string): Promise => { +export const getFavoriteGame = async ( + gameId: string, +): Promise => { const favorites = await getFavoriteGames() return favorites.find((favorite) => favorite.id === gameId) } From 2d5067ab463c9a2af0f0f75e8e6f8008a9b6f206 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Sat, 9 Aug 2025 19:28:32 -0400 Subject: [PATCH 4/4] fix: lint errors --- src/components/Profile/GameList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Profile/GameList.tsx b/src/components/Profile/GameList.tsx index b3e62f12..a84523ab 100644 --- a/src/components/Profile/GameList.tsx +++ b/src/components/Profile/GameList.tsx @@ -104,8 +104,8 @@ export const GameList = ({ if (showCustom) { setCustomAnalyses(getCustomAnalysesAsWebGames()) } - // Load favorites asynchronously - getFavoritesAsWebGames() + // Load favorites (supports both sync and async implementations) + Promise.resolve(getFavoritesAsWebGames()) .then((favorites) => { setFavoriteGames(favorites) setFavoritedGameIds(new Set(favorites.map((f) => f.id)))