Skip to content

Commit 50b5575

Browse files
feat: add initial migration to backend storage for favourited games
1 parent 876ceb9 commit 50b5575

5 files changed

Lines changed: 295 additions & 96 deletions

File tree

__tests__/lib/favorites.test.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ Object.defineProperty(window, 'localStorage', {
3131
value: localStorageMock,
3232
})
3333

34+
// Mock the API functions to test fallback to localStorage
35+
jest.mock('src/api/analysis/analysis', () => ({
36+
updateGameMetadata: jest.fn().mockRejectedValue(new Error('API not available')),
37+
getAnalysisGameList: jest.fn().mockRejectedValue(new Error('API not available')),
38+
}))
39+
3440
describe('favorites', () => {
3541
beforeEach(() => {
3642
localStorageMock.clear()
@@ -44,74 +50,74 @@ describe('favorites', () => {
4450
}
4551

4652
describe('addFavoriteGame', () => {
47-
it('should add a game to favorites with default name', () => {
48-
const favorite = addFavoriteGame(mockGame)
53+
it('should add a game to favorites with default name', async () => {
54+
const favorite = await addFavoriteGame(mockGame)
4955

5056
expect(favorite.id).toBe(mockGame.id)
5157
expect(favorite.customName).toBe(mockGame.label)
5258
expect(favorite.originalLabel).toBe(mockGame.label)
53-
expect(isFavoriteGame(mockGame.id)).toBe(true)
59+
expect(await isFavoriteGame(mockGame.id)).toBe(true)
5460
})
5561

56-
it('should add a game to favorites with custom name', () => {
62+
it('should add a game to favorites with custom name', async () => {
5763
const customName = 'My Best Game'
58-
const favorite = addFavoriteGame(mockGame, customName)
64+
const favorite = await addFavoriteGame(mockGame, customName)
5965

6066
expect(favorite.customName).toBe(customName)
6167
expect(favorite.originalLabel).toBe(mockGame.label)
6268
})
6369

64-
it('should update existing favorite when added again', () => {
65-
addFavoriteGame(mockGame, 'First Name')
66-
addFavoriteGame(mockGame, 'Updated Name')
70+
it('should update existing favorite when added again', async () => {
71+
await addFavoriteGame(mockGame, 'First Name')
72+
await addFavoriteGame(mockGame, 'Updated Name')
6773

68-
const favorites = getFavoriteGames()
74+
const favorites = await getFavoriteGames()
6975
expect(favorites).toHaveLength(1)
7076
expect(favorites[0].customName).toBe('Updated Name')
7177
})
7278
})
7379

7480
describe('removeFavoriteGame', () => {
75-
it('should remove a game from favorites', () => {
76-
addFavoriteGame(mockGame)
77-
expect(isFavoriteGame(mockGame.id)).toBe(true)
81+
it('should remove a game from favorites', async () => {
82+
await addFavoriteGame(mockGame)
83+
expect(await isFavoriteGame(mockGame.id)).toBe(true)
7884

79-
removeFavoriteGame(mockGame.id)
80-
expect(isFavoriteGame(mockGame.id)).toBe(false)
85+
await removeFavoriteGame(mockGame.id, mockGame.type)
86+
expect(await isFavoriteGame(mockGame.id)).toBe(false)
8187
})
8288
})
8389

8490
describe('updateFavoriteName', () => {
85-
it('should update favorite name', () => {
86-
addFavoriteGame(mockGame, 'Original Name')
87-
updateFavoriteName(mockGame.id, 'New Name')
91+
it('should update favorite name', async () => {
92+
await addFavoriteGame(mockGame, 'Original Name')
93+
await updateFavoriteName(mockGame.id, 'New Name', mockGame.type)
8894

89-
const favorite = getFavoriteGame(mockGame.id)
95+
const favorite = await getFavoriteGame(mockGame.id)
9096
expect(favorite?.customName).toBe('New Name')
9197
})
9298

93-
it('should do nothing if favorite does not exist', () => {
94-
const initialFavorites = getFavoriteGames()
95-
updateFavoriteName('non-existent', 'New Name')
99+
it('should do nothing if favorite does not exist', async () => {
100+
const initialFavorites = await getFavoriteGames()
101+
await updateFavoriteName('non-existent', 'New Name')
96102

97-
expect(getFavoriteGames()).toEqual(initialFavorites)
103+
expect(await getFavoriteGames()).toEqual(initialFavorites)
98104
})
99105
})
100106

101107
describe('getFavoritesAsWebGames', () => {
102-
it('should convert favorites to web games', () => {
108+
it('should convert favorites to web games', async () => {
103109
const customName = 'Custom Game Name'
104-
addFavoriteGame(mockGame, customName)
110+
await addFavoriteGame(mockGame, customName)
105111

106-
const webGames = getFavoritesAsWebGames()
112+
const webGames = await getFavoritesAsWebGames()
107113
expect(webGames).toHaveLength(1)
108114
expect(webGames[0].label).toBe(customName)
109115
expect(webGames[0].id).toBe(mockGame.id)
110116
})
111117
})
112118

113119
describe('storage limits', () => {
114-
it('should limit favorites to 100 entries', () => {
120+
it('should limit favorites to 100 entries', async () => {
115121
// Add 101 favorites
116122
for (let i = 0; i < 101; i++) {
117123
const game: AnalysisWebGame = {
@@ -120,10 +126,10 @@ describe('favorites', () => {
120126
label: `Game ${i}`,
121127
result: '1-0',
122128
}
123-
addFavoriteGame(game)
129+
await addFavoriteGame(game)
124130
}
125131

126-
const favorites = getFavoriteGames()
132+
const favorites = await getFavoriteGames()
127133
expect(favorites).toHaveLength(100)
128134
// Latest should be at the top
129135
expect(favorites[0].id).toBe('game-100')

src/api/analysis/analysis.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const getAnalysisGameList = async (
8080
type = 'play',
8181
page = 1,
8282
lichessId?: string,
83+
favoritesOnly?: boolean,
8384
) => {
8485
const url = buildUrl(`analysis/user/list/${type}/${page}`)
8586
const searchParams = new URLSearchParams()
@@ -88,6 +89,10 @@ export const getAnalysisGameList = async (
8889
searchParams.append('lichess_id', lichessId)
8990
}
9091

92+
if (favoritesOnly !== undefined) {
93+
searchParams.append('favorites_only', String(favoritesOnly))
94+
}
95+
9196
const fullUrl = searchParams.toString()
9297
? `${url}?${searchParams.toString()}`
9398
: url
@@ -718,3 +723,33 @@ export const getEngineAnalysis = async (
718723

719724
return res.json()
720725
}
726+
727+
export interface UpdateGameMetadataRequest {
728+
custom_name?: string
729+
is_favorited?: boolean
730+
}
731+
732+
export const updateGameMetadata = async (
733+
gameType: 'custom' | 'play' | 'hand' | 'brain',
734+
gameId: string,
735+
metadata: UpdateGameMetadataRequest,
736+
): Promise<void> => {
737+
const res = await fetch(
738+
buildUrl(`analysis/update_metadata/${gameType}/${gameId}`),
739+
{
740+
method: 'POST',
741+
headers: {
742+
'Content-Type': 'application/json',
743+
},
744+
body: JSON.stringify(metadata),
745+
},
746+
)
747+
748+
if (res.status === 401) {
749+
throw new Error('Unauthorized')
750+
}
751+
752+
if (!res.ok) {
753+
throw new Error('Failed to update game metadata')
754+
}
755+
}

src/components/Analysis/AnalysisGameList.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,8 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
9292
}
9393
return []
9494
})
95-
const [favoriteGames, setFavoriteGames] = useState(() => {
96-
if (typeof window !== 'undefined') {
97-
return getFavoritesAsWebGames()
98-
}
99-
return []
100-
})
95+
const [favoriteGames, setFavoriteGames] = useState<AnalysisWebGame[]>([])
96+
const [favoritedGameIds, setFavoritedGameIds] = useState<Set<string>>(new Set())
10197
const [hbSubsection, setHbSubsection] = useState<'hand' | 'brain'>('hand')
10298

10399
// Modal state for favoriting
@@ -108,7 +104,14 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
108104

109105
useEffect(() => {
110106
setCustomAnalyses(getCustomAnalysesAsWebGames())
111-
setFavoriteGames(getFavoritesAsWebGames())
107+
// Load favorites asynchronously
108+
getFavoritesAsWebGames().then((favorites) => {
109+
setFavoriteGames(favorites)
110+
setFavoritedGameIds(new Set(favorites.map(f => f.id)))
111+
}).catch(() => {
112+
setFavoriteGames([])
113+
setFavoritedGameIds(new Set())
114+
})
112115
}, [refreshTrigger])
113116

114117
useEffect(() => {
@@ -386,17 +389,21 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
386389
setFavoriteModal({ isOpen: true, game })
387390
}
388391

389-
const handleSaveFavorite = (customName: string) => {
392+
const handleSaveFavorite = async (customName: string) => {
390393
if (favoriteModal.game) {
391-
addFavoriteGame(favoriteModal.game, customName)
392-
setFavoriteGames(getFavoritesAsWebGames())
394+
await addFavoriteGame(favoriteModal.game, customName)
395+
const updatedFavorites = await getFavoritesAsWebGames()
396+
setFavoriteGames(updatedFavorites)
397+
setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id)))
393398
}
394399
}
395400

396-
const handleRemoveFavorite = () => {
401+
const handleRemoveFavorite = async () => {
397402
if (favoriteModal.game) {
398-
removeFavoriteGame(favoriteModal.game.id)
399-
setFavoriteGames(getFavoritesAsWebGames())
403+
await removeFavoriteGame(favoriteModal.game.id, favoriteModal.game.type)
404+
const updatedFavorites = await getFavoritesAsWebGames()
405+
setFavoriteGames(updatedFavorites)
406+
setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id)))
400407
}
401408
}
402409

@@ -533,7 +540,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
533540
<>
534541
{getCurrentGames().map((game, index) => {
535542
const selectedGame = currentId && currentId[0] === game.id
536-
const isFavorited = isFavoriteGame(game.id)
543+
const isFavorited = favoritedGameIds.has(game.id)
537544
return (
538545
<div
539546
key={index}
@@ -709,7 +716,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
709716
onClose={() => setFavoriteModal({ isOpen: false, game: null })}
710717
onSave={handleSaveFavorite}
711718
onRemove={
712-
favoriteModal.game && isFavoriteGame(favoriteModal.game.id)
719+
favoriteModal.game && favoritedGameIds.has(favoriteModal.game.id)
713720
? handleRemoveFavorite
714721
: undefined
715722
}

src/components/Profile/GameList.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,8 @@ export const GameList = ({
6060
}
6161
return []
6262
})
63-
const [favoriteGames, setFavoriteGames] = useState(() => {
64-
if (typeof window !== 'undefined') {
65-
return getFavoritesAsWebGames()
66-
}
67-
return []
68-
})
63+
const [favoriteGames, setFavoriteGames] = useState<AnalysisWebGame[]>([])
64+
const [favoritedGameIds, setFavoritedGameIds] = useState<Set<string>>(new Set())
6965
const [currentPage, setCurrentPage] = useState(1)
7066
const [totalPages, setTotalPages] = useState(1)
7167
const [loading, setLoading] = useState(false)
@@ -103,7 +99,14 @@ export const GameList = ({
10399
if (showCustom) {
104100
setCustomAnalyses(getCustomAnalysesAsWebGames())
105101
}
106-
setFavoriteGames(getFavoritesAsWebGames())
102+
// Load favorites asynchronously
103+
getFavoritesAsWebGames().then((favorites) => {
104+
setFavoriteGames(favorites)
105+
setFavoritedGameIds(new Set(favorites.map(f => f.id)))
106+
}).catch(() => {
107+
setFavoriteGames([])
108+
setFavoritedGameIds(new Set())
109+
})
107110
}, [])
108111

109112
useEffect(() => {
@@ -277,17 +280,21 @@ export const GameList = ({
277280
setFavoriteModal({ isOpen: true, game })
278281
}
279282

280-
const handleSaveFavorite = (customName: string) => {
283+
const handleSaveFavorite = async (customName: string) => {
281284
if (favoriteModal.game) {
282-
addFavoriteGame(favoriteModal.game, customName)
283-
setFavoriteGames(getFavoritesAsWebGames())
285+
await addFavoriteGame(favoriteModal.game, customName)
286+
const updatedFavorites = await getFavoritesAsWebGames()
287+
setFavoriteGames(updatedFavorites)
288+
setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id)))
284289
}
285290
}
286291

287-
const handleRemoveFavorite = () => {
292+
const handleRemoveFavorite = async () => {
288293
if (favoriteModal.game) {
289-
removeFavoriteGame(favoriteModal.game.id)
290-
setFavoriteGames(getFavoritesAsWebGames())
294+
await removeFavoriteGame(favoriteModal.game.id, favoriteModal.game.type)
295+
const updatedFavorites = await getFavoritesAsWebGames()
296+
setFavoriteGames(updatedFavorites)
297+
setFavoritedGameIds(new Set(updatedFavorites.map(f => f.id)))
291298
}
292299
}
293300

@@ -409,7 +416,7 @@ export const GameList = ({
409416
) : (
410417
<>
411418
{getCurrentGames().map((game, index) => {
412-
const isFavorited = isFavoriteGame(game.id)
419+
const isFavorited = favoritedGameIds.has(game.id)
413420
return (
414421
<div
415422
key={index}
@@ -534,7 +541,7 @@ export const GameList = ({
534541
onClose={() => setFavoriteModal({ isOpen: false, game: null })}
535542
onSave={handleSaveFavorite}
536543
onRemove={
537-
favoriteModal.game && isFavoriteGame(favoriteModal.game.id)
544+
favoriteModal.game && favoritedGameIds.has(favoriteModal.game.id)
538545
? handleRemoveFavorite
539546
: undefined
540547
}

0 commit comments

Comments
 (0)