Skip to content

Commit b281024

Browse files
Merge branch 'dev' into feature/migrate-favorites-to-backend
2 parents 48ad017 + 4edda92 commit b281024

10 files changed

Lines changed: 331 additions & 111 deletions

File tree

src/api/analysis/analysis.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import {
1313
import { buildUrl } from '../utils'
1414
import { cpToWinrate } from 'src/lib/stockfish'
1515
import { AvailableMoves } from 'src/types/training'
16+
import { Chess } from 'chess.ts'
17+
import {
18+
saveCustomAnalysis,
19+
getCustomAnalysisById,
20+
} from 'src/lib/customAnalysis'
1621

1722
function buildGameTree(moves: any[], initialFen: string) {
1823
const tree = new GameTree(initialFen)
@@ -405,7 +410,6 @@ const createAnalyzedGameFromPGN = async (
405410
pgn: string,
406411
id?: string,
407412
): Promise<AnalyzedGame> => {
408-
const { Chess } = await import('chess.ts')
409413
const chess = new Chess()
410414

411415
try {
@@ -476,9 +480,7 @@ export const getAnalyzedCustomPGN = async (
476480
pgn: string,
477481
name?: string,
478482
): Promise<AnalyzedGame> => {
479-
const { saveCustomAnalysis } = await import('src/lib/customAnalysis')
480-
481-
const stored = saveCustomAnalysis('pgn', pgn, name)
483+
const stored = await saveCustomAnalysis('pgn', pgn, name)
482484

483485
return createAnalyzedGameFromPGN(pgn, stored.id)
484486
}
@@ -487,7 +489,6 @@ const createAnalyzedGameFromFEN = async (
487489
fen: string,
488490
id?: string,
489491
): Promise<AnalyzedGame> => {
490-
const { Chess } = await import('chess.ts')
491492
const chess = new Chess()
492493

493494
try {
@@ -531,18 +532,14 @@ export const getAnalyzedCustomFEN = async (
531532
fen: string,
532533
name?: string,
533534
): Promise<AnalyzedGame> => {
534-
const { saveCustomAnalysis } = await import('src/lib/customAnalysis')
535-
536-
const stored = saveCustomAnalysis('fen', fen, name)
535+
const stored = await saveCustomAnalysis('fen', fen, name)
537536

538537
return createAnalyzedGameFromFEN(fen, stored.id)
539538
}
540539

541540
export const getAnalyzedCustomGame = async (
542541
id: string,
543542
): Promise<AnalyzedGame> => {
544-
const { getCustomAnalysisById } = await import('src/lib/customAnalysis')
545-
546543
const stored = getCustomAnalysisById(id)
547544
if (!stored) {
548545
throw new Error('Custom analysis not found')
@@ -748,3 +745,40 @@ export const updateGameMetadata = async (
748745
throw new Error('Failed to update game metadata')
749746
}
750747
}
748+
749+
export interface StoreCustomGameRequest {
750+
name?: string
751+
pgn?: string
752+
fen?: string
753+
}
754+
755+
export interface StoredCustomGameResponse {
756+
id: string
757+
name: string
758+
pgn?: string
759+
fen?: string
760+
created_at: string
761+
}
762+
763+
export const storeCustomGame = async (
764+
data: StoreCustomGameRequest,
765+
): Promise<StoredCustomGameResponse> => {
766+
const res = await fetch(buildUrl('analysis/store_custom_game'), {
767+
method: 'POST',
768+
headers: {
769+
'Content-Type': 'application/json',
770+
},
771+
body: JSON.stringify(data),
772+
})
773+
774+
if (res.status === 401) {
775+
throw new Error('Unauthorized')
776+
}
777+
778+
if (!res.ok) {
779+
const errorText = await res.text()
780+
throw new Error(`Failed to store custom game: ${errorText}`)
781+
}
782+
783+
return res.json() as Promise<StoredCustomGameResponse>
784+
}

src/components/Analysis/AnalysisGameList.tsx

Lines changed: 45 additions & 25 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,
@@ -87,14 +87,9 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
8787
hand: {},
8888
brain: {},
8989
favorites: {},
90+
custom: {},
9091
})
9192

92-
const [customAnalyses, setCustomAnalyses] = useState(() => {
93-
if (typeof window !== 'undefined') {
94-
return getCustomAnalysesAsWebGames()
95-
}
96-
return []
97-
})
9893
const [favoriteGames, setFavoriteGames] = useState<AnalysisWebGame[]>([])
9994
const [favoritedGameIds, setFavoritedGameIds] = useState<Set<string>>(new Set())
10095
const [hbSubsection, setHbSubsection] = useState<'hand' | 'brain'>('hand')
@@ -106,7 +101,6 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
106101
}>({ isOpen: false, game: null })
107102

108103
useEffect(() => {
109-
setCustomAnalyses(getCustomAnalysesAsWebGames())
110104
// Load favorites asynchronously
111105
getFavoritesAsWebGames().then((favorites) => {
112106
setFavoriteGames(favorites)
@@ -117,6 +111,12 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
117111
})
118112
}, [refreshTrigger])
119113

114+
useEffect(() => {
115+
ensureMigration().catch((error) => {
116+
console.warn('Failed to migrate custom analyses:', error)
117+
})
118+
}, [])
119+
120120
useEffect(() => {
121121
if (currentId?.[1] === 'custom') {
122122
setSelected('custom')
@@ -129,6 +129,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
129129
play: {},
130130
hand: {},
131131
brain: {},
132+
custom: {},
132133
lichess: {},
133134
tournament: {},
134135
favorites: {},
@@ -144,6 +145,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
144145
play: 1,
145146
hand: 1,
146147
brain: 1,
148+
custom: 1,
147149
lichess: 1,
148150
tournament: 1,
149151
favorites: 1,
@@ -193,11 +195,19 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
193195
setLoadingIndex(null)
194196
}, [selected])
195197

198+
useEffect(() => {
199+
if (selected === 'custom') {
200+
setFetchedCache((prev) => ({
201+
...prev,
202+
custom: {},
203+
}))
204+
}
205+
}, [refreshTrigger, selected])
206+
196207
useEffect(() => {
197208
if (
198209
selected !== 'tournament' &&
199210
selected !== 'lichess' &&
200-
selected !== 'custom' &&
201211
selected !== 'hb'
202212
) {
203213
const isAlreadyFetched = fetchedCache[selected]?.[currentPage]
@@ -227,19 +237,30 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
227237
}))
228238
} else {
229239
// Handle regular games response format
240+
let parsedGames
241+
242+
if (selected === 'custom') {
243+
parsedGames = data.games.map((game: any) => ({
244+
id: game.id,
245+
label: game.name || 'Custom Game',
246+
result: '*',
247+
type: game.pgn ? 'custom-pgn' : 'custom-fen',
248+
pgn: game.pgn,
249+
}))
250+
} else {
230251
const parse = (
231-
game: {
232-
game_id: string
233-
maia_name: string
234-
result: string
235-
player_color: 'white' | 'black'
252+
game: {
253+
game_id: string
254+
maia_name: string
255+
result: string
256+
player_color: 'white' | 'black'
236257
is_favorited?: boolean
237258
custom_name?: string
238-
},
239-
type: string,
240-
) => {
241-
const raw = game.maia_name.replace('_kdd_', ' ')
242-
const maia = raw.charAt(0).toUpperCase() + raw.slice(1)
259+
},
260+
type: string,
261+
) => {
262+
const raw = game.maia_name.replace('_kdd_', ' ')
263+
const maia = raw.charAt(0).toUpperCase() + raw.slice(1)
243264

244265
// Use custom name if available, otherwise generate default label
245266
const defaultLabel = game.player_color === 'white'
@@ -396,13 +417,12 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
396417
} else if (totalPagesCache[selected]) {
397418
setTotalPages(totalPagesCache[selected])
398419
setCurrentPage(currentPagePerTab[selected] || 1)
399-
} else if (
400-
selected === 'lichess' ||
401-
selected === 'tournament' ||
402-
selected === 'custom'
403-
) {
420+
} else if (selected === 'lichess' || selected === 'tournament') {
404421
setTotalPages(1)
405422
setCurrentPage(1)
423+
} else if (selected === 'custom') {
424+
setTotalPages(totalPagesCache['custom'] || 1)
425+
setCurrentPage(currentPagePerTab['custom'] || 1)
406426
} else {
407427
setTotalPages(1)
408428
setCurrentPage(currentPagePerTab[selected] || 1)
@@ -538,7 +558,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
538558
const gameType = hbSubsection === 'hand' ? 'hand' : 'brain'
539559
return gamesByPage[gameType]?.[currentPage] || []
540560
} else if (selected === 'custom') {
541-
return customAnalyses
561+
return gamesByPage['custom']?.[currentPage] || []
542562
} else if (selected === 'lichess') {
543563
return analysisLichessList
544564
} else if (selected === 'favorites') {

src/components/Common/Header.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,19 @@ export const Header: React.FC = () => {
370370
>
371371
Feedback
372372
</a>
373+
{user?.lichessId && (
374+
<>
375+
<Link href="/profile" className="uppercase">
376+
Profile
377+
</Link>
378+
<Link href="/settings" className="uppercase">
379+
Settings
380+
</Link>
381+
<button onClick={logout} className="uppercase text-left">
382+
Logout
383+
</button>
384+
</>
385+
)}
373386
</div>
374387
<div className="flex w-full flex-row items-center gap-3 px-4">
375388
<a
@@ -385,7 +398,20 @@ export const Header: React.FC = () => {
385398
loading={leaderboardLoading}
386399
/>
387400
)}
388-
{userInfo}
401+
{user?.lichessId ? (
402+
<div className="flex items-center gap-2">
403+
<span className="material-symbols-outlined text-xl text-primary/80">
404+
account_circle
405+
</span>
406+
<span className="text-sm font-medium text-primary/90">
407+
{user?.displayName}
408+
</span>
409+
</div>
410+
) : (
411+
<button onClick={connectLichess} className="uppercase">
412+
Sign in
413+
</button>
414+
)}
389415
</div>
390416
</div>
391417
)}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
interface ResignationConfirmModalProps {
2+
isOpen: boolean
3+
onClose: () => void
4+
onConfirm: () => void
5+
}
6+
7+
export const ResignationConfirmModal: React.FC<
8+
ResignationConfirmModalProps
9+
> = ({ isOpen, onClose, onConfirm }) => {
10+
if (!isOpen) return null
11+
12+
const handleConfirm = () => {
13+
onConfirm()
14+
onClose()
15+
}
16+
17+
return (
18+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
19+
<div className="w-full max-w-sm rounded-lg bg-background-1 p-6 shadow-lg">
20+
<h3 className="mb-4 text-lg font-semibold text-primary">
21+
Confirm Resignation
22+
</h3>
23+
24+
<p className="mb-6 text-sm text-secondary">
25+
Are you sure you want to resign this game? This action cannot be
26+
undone.
27+
</p>
28+
29+
<div className="flex gap-3">
30+
<button
31+
onClick={onClose}
32+
className="flex-1 rounded border border-white border-opacity-20 px-4 py-2 text-sm text-secondary transition hover:bg-background-2"
33+
>
34+
Cancel
35+
</button>
36+
<button
37+
onClick={handleConfirm}
38+
className="flex-1 rounded bg-red-600 px-4 py-2 text-sm text-white transition hover:bg-red-700"
39+
>
40+
Resign
41+
</button>
42+
</div>
43+
</div>
44+
</div>
45+
)
46+
}

src/components/Common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export * from './ModalContainer'
1717
export * from './ContinueAgainstMaia'
1818
export * from './AnimatedNumber'
1919
export * from './DownloadModelModal'
20+
export * from './ResignationConfirmModal'

src/components/Play/HandBrainPlayControls.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/* eslint-disable @next/next/no-img-element */
22
/* eslint-disable jsx-a11y/alt-text */
3+
import { useState } from 'react'
34
import { PieceSymbol } from 'chess.ts'
45

56
import { BaseGame, Color } from 'src/types'
7+
import { ResignationConfirmModal } from 'src/components'
68

79
const pieceTypes: PieceSymbol[] = ['k', 'q', 'r', 'b', 'n', 'p']
810

@@ -52,6 +54,17 @@ export const HandBrainPlayControls: React.FC<Props> = ({
5254
simulateMaiaTime,
5355
setSimulateMaiaTime,
5456
}: Props) => {
57+
const [showResignConfirm, setShowResignConfirm] = useState(false)
58+
59+
const handleResignClick = () => {
60+
setShowResignConfirm(true)
61+
}
62+
63+
const handleConfirmResign = () => {
64+
if (resign) {
65+
resign()
66+
}
67+
}
5568
const status = playerActive
5669
? isBrain
5770
? selectedPiece
@@ -218,7 +231,7 @@ export const HandBrainPlayControls: React.FC<Props> = ({
218231
{/* Resign Button - Smaller and Less Prominent */}
219232
<div className="flex justify-center">
220233
<button
221-
onClick={resign}
234+
onClick={handleResignClick}
222235
disabled={!resign || !playerActive}
223236
className={`rounded px-3 py-1 text-xs font-medium transition-colors duration-200 ${
224237
resign && playerActive
@@ -234,6 +247,12 @@ export const HandBrainPlayControls: React.FC<Props> = ({
234247
</>
235248
)}
236249
</div>
250+
251+
<ResignationConfirmModal
252+
isOpen={showResignConfirm}
253+
onClose={() => setShowResignConfirm(false)}
254+
onConfirm={handleConfirmResign}
255+
/>
237256
</div>
238257
)
239258
}

0 commit comments

Comments
 (0)