Skip to content

Commit 8ff2bcb

Browse files
feat: consolidate components + clean up training page
1 parent 688da4a commit 8ff2bcb

9 files changed

Lines changed: 501 additions & 41 deletions

File tree

src/components/Board/GameplayTreeInterface.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
AnalysisMovesContainer,
2222
PlayBoardController,
2323
PromotionOverlay,
24+
TreeGameBoard,
2425
} from 'src/components'
2526
import { useUnload } from 'src/hooks/useUnload'
2627
import { PlayTreeControllerContext } from 'src/contexts/PlayTreeControllerContext/PlayTreeControllerContext'
@@ -199,7 +200,7 @@ export const GameplayTreeInterface: React.FC<React.PropsWithChildren<Props>> = (
199200
</div>
200201
</div>
201202
<div className="relative flex aspect-square w-full max-w-[75vh]">
202-
<GameBoard
203+
<TreeGameBoard
203204
game={game}
204205
moves={moveMap}
205206
setCurrentMove={setCurrentMove}
@@ -259,7 +260,7 @@ export const GameplayTreeInterface: React.FC<React.PropsWithChildren<Props>> = (
259260
) : null}
260261
</div>
261262
<div className="relative flex aspect-square h-[100vw] w-screen">
262-
<GameBoard
263+
<TreeGameBoard
263264
game={game}
264265
moves={moveMap}
265266
setCurrentMove={setCurrentMove}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { useWindowSize } from 'src/hooks'
2+
import { FlipIcon } from 'src/components/Icons/icons'
3+
import { useCallback, useContext, useEffect, useMemo } from 'react'
4+
import {
5+
AnalysisGameControllerContext,
6+
TuringTreeControllerContext,
7+
} from 'src/contexts/'
8+
import { PlayTreeControllerContext } from 'src/contexts/PlayTreeControllerContext/PlayTreeControllerContext'
9+
import { TrainingTreeControllerContext } from 'src/contexts/TrainingTreeControllerContext/TrainingTreeControllerContext'
10+
11+
interface Props {
12+
setCurrentMove?: (move: [string, string] | null) => void
13+
}
14+
15+
interface TreeController {
16+
orientation: 'white' | 'black'
17+
setOrientation: (orientation: 'white' | 'black') => void
18+
currentNode?: any
19+
currentIndex?: number
20+
plyCount: number
21+
goToNode?: (node: any) => void
22+
goToNextNode: () => void
23+
goToPreviousNode: () => void
24+
goToRootNode: () => void
25+
setCurrentIndex?: (index: number) => void
26+
gameTree?: any
27+
}
28+
29+
export const TreeBoardController: React.FC<Props> = ({
30+
setCurrentMove,
31+
}: Props) => {
32+
const { width } = useWindowSize()
33+
34+
// Try to get context from any available tree controller
35+
const analysisCtx = useContext(AnalysisGameControllerContext)
36+
const playCtx = useContext(PlayTreeControllerContext)
37+
const turingCtx = useContext(TuringTreeControllerContext)
38+
const trainingCtx = useContext(TrainingTreeControllerContext)
39+
40+
// Determine which context to use (first available one)
41+
const controller: TreeController = useMemo(() => {
42+
// Check if we have a valid training context (highest priority for training pages)
43+
if (trainingCtx && trainingCtx.currentNode) {
44+
return trainingCtx
45+
}
46+
// Check if we have a valid analysis context
47+
if (analysisCtx && analysisCtx.currentNode !== undefined) {
48+
return analysisCtx
49+
}
50+
// Check if we have a valid play context
51+
if (playCtx && playCtx.currentNode !== undefined) {
52+
return playCtx
53+
}
54+
// Check if we have a valid turing context
55+
if (turingCtx && turingCtx.currentNode !== undefined) {
56+
return turingCtx
57+
}
58+
// Fallback to analysis context (should not happen in practice)
59+
return analysisCtx
60+
}, [analysisCtx, playCtx, turingCtx, trainingCtx])
61+
62+
const {
63+
orientation,
64+
setOrientation,
65+
currentNode,
66+
currentIndex,
67+
goToNode,
68+
goToNextNode,
69+
goToPreviousNode,
70+
goToRootNode,
71+
plyCount,
72+
setCurrentIndex,
73+
gameTree,
74+
} = controller
75+
76+
const toggleBoardOrientation = useCallback(() => {
77+
setOrientation(orientation === 'white' ? 'black' : 'white')
78+
}, [orientation, setOrientation])
79+
80+
// Determine navigation state based on available data
81+
const hasPrevious = useMemo(() => {
82+
if (currentNode !== undefined) {
83+
return !!currentNode?.parent
84+
}
85+
if (currentIndex !== undefined) {
86+
return currentIndex > 0
87+
}
88+
return false
89+
}, [currentNode, currentIndex])
90+
91+
const hasNext = useMemo(() => {
92+
if (currentNode !== undefined) {
93+
return !!currentNode?.mainChild
94+
}
95+
if (currentIndex !== undefined) {
96+
return currentIndex < plyCount - 1
97+
}
98+
return false
99+
}, [currentNode, currentIndex, plyCount])
100+
101+
const getFirst = useCallback(() => {
102+
goToRootNode()
103+
setCurrentMove?.(null)
104+
}, [goToRootNode, setCurrentMove])
105+
106+
const getPrevious = useCallback(() => {
107+
goToPreviousNode()
108+
setCurrentMove?.(null)
109+
}, [goToPreviousNode, setCurrentMove])
110+
111+
const getNext = useCallback(() => {
112+
goToNextNode()
113+
setCurrentMove?.(null)
114+
}, [goToNextNode, setCurrentMove])
115+
116+
const getLast = useCallback(() => {
117+
if (currentNode && goToNode) {
118+
// Node-based navigation
119+
let lastNode = currentNode
120+
while (lastNode?.mainChild) {
121+
lastNode = lastNode.mainChild
122+
}
123+
if (lastNode) {
124+
goToNode(lastNode)
125+
}
126+
} else if (gameTree && setCurrentIndex) {
127+
// Index-based navigation (Turing)
128+
const mainLine = gameTree.getMainLine()
129+
if (mainLine.length > 0) {
130+
setCurrentIndex(mainLine.length - 1)
131+
}
132+
}
133+
setCurrentMove?.(null)
134+
}, [currentNode, goToNode, gameTree, setCurrentIndex, setCurrentMove])
135+
136+
useEffect(() => {
137+
if (width <= 670) return
138+
139+
const onKeyDown = (e: KeyboardEvent) => {
140+
switch (e.key) {
141+
case 'Left':
142+
case 'ArrowLeft':
143+
if (hasPrevious) getPrevious()
144+
break
145+
case 'Right':
146+
case 'ArrowRight':
147+
if (hasNext) getNext()
148+
break
149+
case 'Down':
150+
case 'ArrowDown':
151+
if (hasNext) getLast()
152+
break
153+
case 'Up':
154+
case 'ArrowUp':
155+
if (hasPrevious) getFirst()
156+
break
157+
case 'f':
158+
toggleBoardOrientation()
159+
break
160+
default:
161+
}
162+
}
163+
164+
window.addEventListener('keydown', onKeyDown)
165+
return () => window.removeEventListener('keydown', onKeyDown)
166+
}, [
167+
width,
168+
hasPrevious,
169+
hasNext,
170+
getPrevious,
171+
getNext,
172+
getFirst,
173+
getLast,
174+
toggleBoardOrientation,
175+
])
176+
177+
return (
178+
<div className="flex w-full flex-row items-center gap-[1px] md:rounded">
179+
<button
180+
onClick={toggleBoardOrientation}
181+
className="flex h-7 flex-1 items-center justify-center bg-button-secondary transition duration-200 hover:bg-human-3 disabled:bg-button-secondary/40 md:rounded-sm"
182+
>
183+
{FlipIcon}
184+
</button>
185+
<button
186+
onClick={getFirst}
187+
disabled={!hasPrevious}
188+
className="flex h-7 flex-1 items-center justify-center bg-button-secondary transition duration-200 hover:bg-human-3 disabled:bg-button-secondary/40 md:rounded-sm"
189+
>
190+
&#8249;&#8249;&#8249;
191+
</button>
192+
<button
193+
onClick={getPrevious}
194+
disabled={!hasPrevious}
195+
className="flex h-7 flex-1 items-center justify-center bg-button-secondary transition duration-200 hover:bg-human-3 disabled:bg-button-secondary/40 md:rounded-sm"
196+
>
197+
&#8249;
198+
</button>
199+
<button
200+
onClick={getNext}
201+
disabled={!hasNext}
202+
className="flex h-7 flex-1 items-center justify-center bg-button-secondary transition duration-200 hover:bg-human-3 disabled:bg-button-secondary/40 md:rounded-sm"
203+
>
204+
&#8250;
205+
</button>
206+
<button
207+
onClick={getLast}
208+
disabled={!hasNext}
209+
className="flex h-7 flex-1 items-center justify-center bg-button-secondary transition duration-200 hover:bg-human-3 disabled:bg-button-secondary/40 md:rounded-sm"
210+
>
211+
&#8250;&#8250;&#8250;
212+
</button>
213+
</div>
214+
)
215+
}

0 commit comments

Comments
 (0)