Skip to content

Commit 18d5563

Browse files
feat: add initial backend api implementation of custom game storage
1 parent e551c9d commit 18d5563

3 files changed

Lines changed: 204 additions & 69 deletions

File tree

src/api/analysis/analysis.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ export const getAnalyzedCustomPGN = async (
478478
): Promise<AnalyzedGame> => {
479479
const { saveCustomAnalysis } = await import('src/lib/customAnalysis')
480480

481-
const stored = saveCustomAnalysis('pgn', pgn, name)
481+
const stored = await saveCustomAnalysis('pgn', pgn, name)
482482

483483
return createAnalyzedGameFromPGN(pgn, stored.id)
484484
}
@@ -533,7 +533,7 @@ export const getAnalyzedCustomFEN = async (
533533
): Promise<AnalyzedGame> => {
534534
const { saveCustomAnalysis } = await import('src/lib/customAnalysis')
535535

536-
const stored = saveCustomAnalysis('fen', fen, name)
536+
const stored = await saveCustomAnalysis('fen', fen, name)
537537

538538
return createAnalyzedGameFromFEN(fen, stored.id)
539539
}
@@ -718,3 +718,40 @@ export const getEngineAnalysis = async (
718718

719719
return res.json()
720720
}
721+
722+
export interface StoreCustomGameRequest {
723+
name?: string
724+
pgn?: string
725+
fen?: string
726+
}
727+
728+
export interface StoredCustomGameResponse {
729+
id: string
730+
name: string
731+
pgn?: string
732+
fen?: string
733+
created_at: string
734+
}
735+
736+
export const storeCustomGame = async (
737+
data: StoreCustomGameRequest,
738+
): Promise<StoredCustomGameResponse> => {
739+
const res = await fetch(buildUrl('analysis/store_custom_game'), {
740+
method: 'POST',
741+
headers: {
742+
'Content-Type': 'application/json',
743+
},
744+
body: JSON.stringify(data),
745+
})
746+
747+
if (res.status === 401) {
748+
throw new Error('Unauthorized')
749+
}
750+
751+
if (!res.ok) {
752+
const errorText = await res.text()
753+
throw new Error(`Failed to store custom game: ${errorText}`)
754+
}
755+
756+
return res.json() as Promise<StoredCustomGameResponse>
757+
}

src/components/Analysis/AnalysisGameList.tsx

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Tournament } from 'src/components'
1212
import { FavoriteModal } from 'src/components/Common/FavoriteModal'
1313
import { AnalysisListContext } from 'src/contexts'
1414
import { getAnalysisGameList } from 'src/api'
15-
import { getCustomAnalysesAsWebGames } from 'src/lib/customAnalysis'
15+
import { ensureMigration } from 'src/lib/customAnalysis'
1616
import {
1717
getFavoritesAsWebGames,
1818
addFavoriteGame,
@@ -84,14 +84,9 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
8484
play: {},
8585
hand: {},
8686
brain: {},
87+
custom: {},
8788
})
8889

89-
const [customAnalyses, setCustomAnalyses] = useState(() => {
90-
if (typeof window !== 'undefined') {
91-
return getCustomAnalysesAsWebGames()
92-
}
93-
return []
94-
})
9590
const [favoriteGames, setFavoriteGames] = useState(() => {
9691
if (typeof window !== 'undefined') {
9792
return getFavoritesAsWebGames()
@@ -107,10 +102,15 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
107102
}>({ isOpen: false, game: null })
108103

109104
useEffect(() => {
110-
setCustomAnalyses(getCustomAnalysesAsWebGames())
111105
setFavoriteGames(getFavoritesAsWebGames())
112106
}, [refreshTrigger])
113107

108+
useEffect(() => {
109+
ensureMigration().catch((error) => {
110+
console.warn('Failed to migrate custom analyses:', error)
111+
})
112+
}, [])
113+
114114
useEffect(() => {
115115
if (currentId?.[1] === 'custom') {
116116
setSelected('custom')
@@ -123,6 +123,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
123123
play: {},
124124
hand: {},
125125
brain: {},
126+
custom: {},
126127
lichess: {},
127128
tournament: {},
128129
})
@@ -137,6 +138,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
137138
play: 1,
138139
hand: 1,
139140
brain: 1,
141+
custom: 1,
140142
lichess: 1,
141143
tournament: 1,
142144
})
@@ -185,11 +187,19 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
185187
setLoadingIndex(null)
186188
}, [selected])
187189

190+
useEffect(() => {
191+
if (selected === 'custom') {
192+
setFetchedCache((prev) => ({
193+
...prev,
194+
custom: {},
195+
}))
196+
}
197+
}, [refreshTrigger, selected])
198+
188199
useEffect(() => {
189200
if (
190201
selected !== 'tournament' &&
191202
selected !== 'lichess' &&
192-
selected !== 'custom' &&
193203
selected !== 'hb' &&
194204
selected !== 'favorites'
195205
) {
@@ -205,32 +215,44 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
205215

206216
getAnalysisGameList(selected, currentPage)
207217
.then((data) => {
208-
const parse = (
209-
game: {
210-
game_id: string
211-
maia_name: string
212-
result: string
213-
player_color: 'white' | 'black'
214-
},
215-
type: string,
216-
) => {
217-
const raw = game.maia_name.replace('_kdd_', ' ')
218-
const maia = raw.charAt(0).toUpperCase() + raw.slice(1)
219-
220-
return {
221-
id: game.game_id,
222-
label:
223-
game.player_color === 'white'
224-
? `You vs. ${maia}`
225-
: `${maia} vs. You`,
226-
result: game.result,
227-
type,
218+
let parsedGames
219+
220+
if (selected === 'custom') {
221+
parsedGames = data.games.map((game: any) => ({
222+
id: game.id,
223+
label: game.name || 'Custom Game',
224+
result: '*',
225+
type: game.pgn ? 'custom-pgn' : 'custom-fen',
226+
pgn: game.pgn,
227+
}))
228+
} else {
229+
const parse = (
230+
game: {
231+
game_id: string
232+
maia_name: string
233+
result: string
234+
player_color: 'white' | 'black'
235+
},
236+
type: string,
237+
) => {
238+
const raw = game.maia_name.replace('_kdd_', ' ')
239+
const maia = raw.charAt(0).toUpperCase() + raw.slice(1)
240+
241+
return {
242+
id: game.game_id,
243+
label:
244+
game.player_color === 'white'
245+
? `You vs. ${maia}`
246+
: `${maia} vs. You`,
247+
result: game.result,
248+
type,
249+
}
228250
}
229-
}
230251

231-
const parsedGames = data.games.map((game: GameData) =>
232-
parse(game, selected),
233-
)
252+
parsedGames = data.games.map((game: GameData) =>
253+
parse(game, selected),
254+
)
255+
}
234256
const calculatedTotalPages =
235257
data.total_pages || Math.ceil(data.total_games / 25)
236258

@@ -345,13 +367,12 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
345367
} else if (totalPagesCache[selected]) {
346368
setTotalPages(totalPagesCache[selected])
347369
setCurrentPage(currentPagePerTab[selected] || 1)
348-
} else if (
349-
selected === 'lichess' ||
350-
selected === 'tournament' ||
351-
selected === 'custom'
352-
) {
370+
} else if (selected === 'lichess' || selected === 'tournament') {
353371
setTotalPages(1)
354372
setCurrentPage(1)
373+
} else if (selected === 'custom') {
374+
setTotalPages(totalPagesCache['custom'] || 1)
375+
setCurrentPage(currentPagePerTab['custom'] || 1)
355376
} else {
356377
setTotalPages(1)
357378
setCurrentPage(currentPagePerTab[selected] || 1)
@@ -407,7 +428,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
407428
const gameType = hbSubsection === 'hand' ? 'hand' : 'brain'
408429
return gamesByPage[gameType]?.[currentPage] || []
409430
} else if (selected === 'custom') {
410-
return customAnalyses
431+
return gamesByPage['custom']?.[currentPage] || []
411432
} else if (selected === 'lichess') {
412433
return analysisLichessList
413434
} else if (selected === 'favorites') {

0 commit comments

Comments
 (0)