Skip to content

Commit 570f73a

Browse files
feat: add broadcasts to home page
1 parent 399c3af commit 570f73a

4 files changed

Lines changed: 439 additions & 2 deletions

File tree

src/components/Home/HomeHero.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ 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 { LiveChessBoard } from 'src/components/Home/LiveChessBoard'
2221

2322
interface Props {
2423
scrollHandler: () => void
@@ -292,7 +291,6 @@ export const HomeHero: React.FC<Props> = ({ scrollHandler }: Props) => {
292291
<></>
293292
)}
294293
</motion.div>
295-
<LiveChessBoard />
296294
</div>
297295
</Fragment>
298296
)
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import React, { useState, useEffect, useCallback, useRef } from 'react'
2+
import { useRouter } from 'next/router'
3+
import { 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+
import { StreamedGame, StreamedMove } from 'src/types/stream'
8+
9+
interface LiveGameData {
10+
gameId: string
11+
white?: {
12+
user: {
13+
id: string
14+
name: string
15+
}
16+
rating?: number
17+
}
18+
black?: {
19+
user: {
20+
id: string
21+
name: string
22+
}
23+
rating?: number
24+
}
25+
currentFen?: string
26+
isLive?: boolean
27+
}
28+
29+
export const LiveChessBoardShowcase: React.FC = () => {
30+
const router = useRouter()
31+
const [liveGame, setLiveGame] = useState<LiveGameData | null>(null)
32+
const [currentFen, setCurrentFen] = useState<string>(
33+
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
34+
)
35+
const [error, setError] = useState<string | null>(null)
36+
const abortController = useRef<AbortController | null>(null)
37+
38+
const handleGameStart = useCallback((gameData: StreamedGame) => {
39+
if (gameData.fen) {
40+
setCurrentFen(gameData.fen)
41+
}
42+
setLiveGame({
43+
gameId: gameData.id,
44+
white: gameData.players?.white,
45+
black: gameData.players?.black,
46+
currentFen: gameData.fen,
47+
isLive: true,
48+
})
49+
}, [])
50+
51+
const handleMove = useCallback((moveData: StreamedMove) => {
52+
if (moveData.fen) {
53+
setCurrentFen(moveData.fen)
54+
}
55+
}, [])
56+
57+
const handleStreamComplete = useCallback(() => {
58+
console.log('Live board showcase - Stream completed')
59+
fetchNewGame()
60+
}, [])
61+
62+
const fetchNewGame = useCallback(async () => {
63+
try {
64+
setError(null)
65+
const tvGame = await getLichessTVGame()
66+
67+
// Stop current stream if any
68+
if (abortController.current) {
69+
abortController.current.abort()
70+
}
71+
72+
// Start new stream
73+
abortController.current = new AbortController()
74+
75+
setLiveGame({
76+
gameId: tvGame.gameId,
77+
white: tvGame.white,
78+
black: tvGame.black,
79+
isLive: true,
80+
})
81+
82+
streamLichessGame(
83+
tvGame.gameId,
84+
handleGameStart,
85+
handleMove,
86+
handleStreamComplete,
87+
abortController.current.signal,
88+
).catch((err) => {
89+
if (err.name !== 'AbortError') {
90+
console.error('Live board streaming error:', err)
91+
setError('Connection lost')
92+
}
93+
})
94+
} catch (err) {
95+
console.error('Error fetching new live game:', err)
96+
setError('Failed to load live game')
97+
}
98+
}, [handleGameStart, handleMove, handleStreamComplete])
99+
100+
useEffect(() => {
101+
// Initial fetch
102+
fetchNewGame()
103+
104+
// Cleanup on unmount
105+
return () => {
106+
if (abortController.current) {
107+
abortController.current.abort()
108+
}
109+
}
110+
}, []) // Remove fetchNewGame dependency to prevent re-renders
111+
112+
const handleClick = () => {
113+
if (liveGame?.gameId) {
114+
router.push(`/analysis/stream/${liveGame.gameId}`)
115+
}
116+
}
117+
118+
// Keep FEN only; Chessground renders from FEN directly
119+
120+
return (
121+
<div className="flex flex-col items-center">
122+
<motion.div
123+
className="relative inline-block h-[192px] w-[192px] cursor-pointer overflow-hidden rounded-lg transition-colors duration-200"
124+
initial={{ opacity: 0, scale: 0.95 }}
125+
animate={{ opacity: 1, scale: 1 }}
126+
transition={{ duration: 0.3 }}
127+
onClick={handleClick}
128+
>
129+
{/* Live indicator */}
130+
{liveGame?.isLive && (
131+
<div className="absolute right-2 top-2 z-10 flex items-center gap-1 rounded-full bg-red-500 px-2 py-1">
132+
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-white" />
133+
<span className="text-xs font-semibold text-white">LIVE</span>
134+
</div>
135+
)}
136+
137+
{/* Chess board */}
138+
<Chessground
139+
contained
140+
width={192}
141+
height={192}
142+
config={{
143+
fen: currentFen,
144+
viewOnly: true,
145+
coordinates: false,
146+
drawable: {
147+
enabled: false,
148+
},
149+
highlight: {
150+
lastMove: true,
151+
check: true,
152+
},
153+
animation: {
154+
enabled: true,
155+
duration: 200,
156+
},
157+
}}
158+
/>
159+
</motion.div>
160+
161+
{/* Player names below the board */}
162+
{liveGame && (
163+
<div className="mt-2 w-48 text-center">
164+
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-xs">
165+
<div className="flex min-w-0 items-center gap-1">
166+
<div className="h-1.5 w-1.5 rounded-full border border-gray-300 bg-white" />
167+
<span className="truncate font-medium">
168+
{liveGame.white?.user?.name || 'White'}
169+
</span>
170+
{liveGame.white?.rating && (
171+
<span className="shrink-0 text-[11px] text-secondary">
172+
({liveGame.white.rating})
173+
</span>
174+
)}
175+
</div>
176+
<span className="text-[11px] text-secondary">vs</span>
177+
<div className="flex min-w-0 items-center justify-end gap-1">
178+
<div className="h-1.5 w-1.5 rounded-full bg-black" />
179+
<span className="truncate font-medium">
180+
{liveGame.black?.user?.name || 'Black'}
181+
</span>
182+
{liveGame.black?.rating && (
183+
<span className="shrink-0 text-[11px] text-secondary">
184+
({liveGame.black.rating})
185+
</span>
186+
)}
187+
</div>
188+
</div>
189+
</div>
190+
)}
191+
192+
{error && (
193+
<div className="mt-3 text-center">
194+
<p className="text-sm text-red-400">{error}</p>
195+
<button
196+
onClick={fetchNewGame}
197+
className="mt-1 text-xs font-medium text-human-4 hover:underline"
198+
>
199+
Retry
200+
</button>
201+
</div>
202+
)}
203+
</div>
204+
)
205+
}

0 commit comments

Comments
 (0)