Skip to content

Commit 7b6c2b1

Browse files
feat: improve live chessboard analysis on home page
1 parent f8be3e5 commit 7b6c2b1

File tree

3 files changed

+249
-13
lines changed

3 files changed

+249
-13
lines changed

src/components/Home/HomeHero.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { PlayType } from 'src/types'
1818
import { getGlobalStats, getActiveUserCount } from 'src/api'
1919
import { AuthContext, ModalContext } from 'src/contexts'
2020
import { AnimatedNumber } from 'src/components/Common/AnimatedNumber'
21-
import { LiveChessWidget } from 'src/components/Home/LiveChessWidget'
21+
import { LiveChessBoard } from 'src/components/Home/LiveChessBoard'
2222

2323
interface Props {
2424
scrollHandler: () => void
@@ -203,17 +203,6 @@ export const HomeHero: React.FC<Props> = ({ scrollHandler }: Props) => {
203203
)}
204204
</motion.div>
205205
</div>
206-
<div className="flex w-full flex-col items-center justify-center gap-4 md:w-auto">
207-
<div className="flex flex-col items-center gap-2">
208-
<h3 className="text-sm font-medium text-secondary">
209-
Watch Live Analysis
210-
</h3>
211-
<LiveChessWidget />
212-
<p className="max-w-[200px] text-center text-xs text-secondary">
213-
Real-time Maia analysis of top-rated games from Lichess
214-
</p>
215-
</div>
216-
</div>
217206
<div className="grid w-full flex-1 grid-cols-1 gap-2 md:grid-cols-3 md:gap-4">
218207
<FeatureCard
219208
icon={<RegularPlayIcon />}
@@ -303,6 +292,7 @@ export const HomeHero: React.FC<Props> = ({ scrollHandler }: Props) => {
303292
<></>
304293
)}
305294
</motion.div>
295+
<LiveChessBoard />
306296
</div>
307297
</Fragment>
308298
)
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import React, { useState, useEffect, useCallback, useRef } from 'react'
2+
import { useRouter } from 'next/router'
3+
import { AnimatePresence, motion } from 'framer-motion'
4+
import { Chess } from 'chess.ts'
5+
import Chessground from '@react-chess/chessground'
6+
import { getLichessTVGame, streamLichessGame } from 'src/api/lichess/streaming'
7+
8+
interface LiveGameData {
9+
gameId: string
10+
white?: {
11+
name: string
12+
rating?: number
13+
}
14+
black?: {
15+
name: string
16+
rating?: number
17+
}
18+
currentFen?: string
19+
isLive?: boolean
20+
}
21+
22+
export const LiveChessBoard: React.FC = () => {
23+
const router = useRouter()
24+
const [liveGame, setLiveGame] = useState<LiveGameData | null>(null)
25+
const [currentFen, setCurrentFen] = useState<string>(
26+
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
27+
)
28+
const [isHovered, setIsHovered] = useState(false)
29+
const [error, setError] = useState<string | null>(null)
30+
const abortController = useRef<AbortController | null>(null)
31+
32+
const handleGameStart = useCallback((gameData: any) => {
33+
console.log('Live board - Game started:', gameData)
34+
if (gameData.fen) {
35+
setCurrentFen(gameData.fen)
36+
}
37+
setLiveGame({
38+
gameId: gameData.id || gameData.gameId,
39+
white: gameData.players?.white?.user || gameData.white,
40+
black: gameData.players?.black?.user || gameData.black,
41+
currentFen: gameData.fen,
42+
isLive: true,
43+
})
44+
}, [])
45+
46+
const handleMove = useCallback((moveData: any) => {
47+
console.log('Live board - New move:', moveData)
48+
if (moveData.fen) {
49+
setCurrentFen(moveData.fen)
50+
}
51+
}, [])
52+
53+
const handleStreamComplete = useCallback(() => {
54+
console.log('Live board - Stream completed')
55+
// Try to get a new live game
56+
fetchNewGame()
57+
}, [])
58+
59+
const fetchNewGame = useCallback(async () => {
60+
try {
61+
setError(null)
62+
const tvGame = await getLichessTVGame()
63+
64+
// Stop current stream if any
65+
if (abortController.current) {
66+
abortController.current.abort()
67+
}
68+
69+
// Start new stream
70+
abortController.current = new AbortController()
71+
72+
setLiveGame({
73+
gameId: tvGame.gameId,
74+
white: tvGame.white,
75+
black: tvGame.black,
76+
isLive: true,
77+
})
78+
79+
// Start streaming the new game
80+
streamLichessGame(
81+
tvGame.gameId,
82+
handleGameStart,
83+
handleMove,
84+
handleStreamComplete,
85+
abortController.current.signal,
86+
).catch((err) => {
87+
if (err.name !== 'AbortError') {
88+
console.error('Live board streaming error:', err)
89+
setError('Connection lost')
90+
}
91+
})
92+
} catch (err) {
93+
console.error('Error fetching new live game:', err)
94+
setError('Failed to load live game')
95+
}
96+
}, [handleGameStart, handleMove, handleStreamComplete])
97+
98+
useEffect(() => {
99+
// Initial fetch
100+
fetchNewGame()
101+
102+
// Cleanup on unmount
103+
return () => {
104+
if (abortController.current) {
105+
abortController.current.abort()
106+
}
107+
}
108+
}, []) // Remove fetchNewGame dependency to prevent re-renders
109+
110+
const handleClick = () => {
111+
if (liveGame?.gameId) {
112+
router.push(`/analysis/stream/${liveGame.gameId}`)
113+
}
114+
}
115+
116+
// Convert FEN to chessground position
117+
const chess = new Chess(currentFen)
118+
const position = chess.board()
119+
const pieces = new Map()
120+
121+
for (let rank = 0; rank < 8; rank++) {
122+
for (let file = 0; file < 8; file++) {
123+
const piece = position[rank][file]
124+
if (piece) {
125+
const square =
126+
String.fromCharCode('a'.charCodeAt(0) + file) + (8 - rank).toString()
127+
const color = piece.color === 'w' ? 'white' : 'black'
128+
const role =
129+
piece.type === 'n'
130+
? 'knight'
131+
: piece.type === 'b'
132+
? 'bishop'
133+
: piece.type === 'r'
134+
? 'rook'
135+
: piece.type === 'q'
136+
? 'queen'
137+
: piece.type === 'k'
138+
? 'king'
139+
: 'pawn'
140+
pieces.set(square, { color, role })
141+
}
142+
}
143+
}
144+
145+
return (
146+
<motion.div
147+
className="absolute bottom-4 left-4 z-20 hidden cursor-pointer md:block"
148+
initial={{ opacity: 0.4, scale: 1.0 }}
149+
animate={{
150+
opacity: isHovered ? 0.9 : 0.4,
151+
scale: isHovered ? 1.01 : 1.0,
152+
}}
153+
transition={{ duration: 0.2 }}
154+
onMouseEnter={() => setIsHovered(true)}
155+
onMouseLeave={() => setIsHovered(false)}
156+
onClick={handleClick}
157+
>
158+
<div className="relative">
159+
{/* Live indicator */}
160+
{liveGame?.isLive && (
161+
<div className="absolute -right-4 -top-4 z-10 flex items-center gap-1 rounded-full bg-red-500 px-2 py-1">
162+
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-white" />
163+
<span className="text-xs font-semibold text-white">LIVE</span>
164+
</div>
165+
)}
166+
167+
{/* Chess board */}
168+
<div className="aspect-square h-24 w-24">
169+
<Chessground
170+
contained
171+
config={{
172+
fen: currentFen,
173+
viewOnly: true,
174+
coordinates: false,
175+
drawable: {
176+
enabled: false,
177+
},
178+
highlight: {
179+
lastMove: true,
180+
check: true,
181+
},
182+
animation: {
183+
enabled: true,
184+
duration: 200,
185+
},
186+
}}
187+
/>
188+
</div>
189+
190+
<AnimatePresence>
191+
{/* Game info on hover */}
192+
{isHovered && liveGame && (
193+
<motion.div
194+
className="absolute left-[calc(100%+0.5rem)] top-3 flex w-48 flex-col items-center justify-center rounded border border-white/10 bg-background-1/60"
195+
initial={{ opacity: 0, x: -10 }}
196+
animate={{ opacity: 1, x: 0 }}
197+
exit={{ opacity: 0, x: -10 }}
198+
transition={{ duration: 0.2 }}
199+
>
200+
<div className="flex w-full items-center justify-center border-b border-white/10 py-1.5">
201+
<span className="text-xs font-semibold uppercase tracking-wider text-human-2">
202+
Lichess TV Analysis
203+
</span>
204+
</div>
205+
<div className="flex flex-row items-center gap-1 px-2 pt-2 text-xxs">
206+
<div className="flex items-center justify-between">
207+
<div className="flex items-center gap-1">
208+
<div className="h-2 w-2 rounded-full border bg-white" />
209+
<span className="font-medium">
210+
{liveGame.white?.name || 'White'}
211+
</span>
212+
{liveGame.white?.rating && (
213+
<span className="text-secondary">
214+
({liveGame.white.rating})
215+
</span>
216+
)}
217+
</div>
218+
</div>
219+
<span className="text-secondary">vs.</span>
220+
<div className="flex items-center justify-between">
221+
<div className="flex items-center gap-1">
222+
<div className="h-2 w-2 rounded-full bg-black" />
223+
<span className="font-medium">
224+
{liveGame.black?.name || 'Black'}
225+
</span>
226+
{liveGame.black?.rating && (
227+
<span className="text-secondary">
228+
({liveGame.black.rating})
229+
</span>
230+
)}
231+
</div>
232+
</div>
233+
</div>
234+
<div className="mt-1.5 flex items-center justify-start px-2 pb-2">
235+
<span className="text-xxs text-secondary">
236+
Click to watch live analysis →
237+
</span>
238+
</div>
239+
</motion.div>
240+
)}
241+
</AnimatePresence>
242+
</div>
243+
</motion.div>
244+
)
245+
}

src/pages/analysis/stream/[gameId].tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ const StreamAnalysisPage: NextPage = () => {
7171
}, [])
7272

7373
// Use the current streaming game or dummy game for analysis controller
74+
// Disable backend analysis saving for stream page since this is live data
7475
const currentGame = streamController.game || dummyGame
75-
const analysisController = useAnalysisController(currentGame)
76+
const analysisController = useAnalysisController(currentGame, undefined, false)
7677

7778
// Set current node to follow live moves
7879
useEffect(() => {

0 commit comments

Comments
 (0)