()
10 | const boardOneStream = useCallback((analysisPosition: TPositionTreeSetter) => {
11 | setBoardTwoPosition(analysisPosition)
12 | }, [])
13 | const boardTwoStream = useCallback((analysisPosition: TPositionTreeSetter) => {
14 | setBoardOnePosition(analysisPosition)
15 | }, [])
16 |
17 | return (
18 |
19 |
47 |
48 | Sync Board
49 | {boardTwoStream(analysisPosition)}}
81 | styles = {{
82 | boardHeaderStyles: {
83 | boardHeaderContainerClassName: 'board-header-container',
84 | boardHeaderTextClassName: 'board-header-text',
85 | boardHeaderTextDetailClassName: 'board-header-text-detail'
86 | },
87 | panelStyles: {
88 | panelContainerClassName: 'panel-container'
89 | }
90 | }}
91 | />
92 |
93 | Board Loaded from FEN
94 |
105 |
106 | )
107 | }
108 |
109 | export default App
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | }
4 |
--------------------------------------------------------------------------------
/src/lib/__snapshots__/index.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`HelloWorld > HelloWorld component renders correctly 1`] = `
4 |
5 | Hello,
6 | World
7 | !
8 |
9 | `;
10 |
11 | exports[`HelloWorld > The greetee prop works 1`] = `
12 |
13 | Hello,
14 | Universe
15 | !
16 |
17 | `;
18 |
19 | exports[`PGNBoard > PGNBoard component renders correctly 1`] = `null`;
20 |
21 | exports[`PGNBoard > The greetee prop works 1`] = `null`;
22 |
--------------------------------------------------------------------------------
/src/lib/components/AnalysisBoard.tsx:
--------------------------------------------------------------------------------
1 |
2 | import Board from "./Board";
3 | import { TBoardConfig } from './Board';
4 |
5 | import BoardHeader, { TBoardHeaderStyles } from "./BoardHeader";
6 | import { TBoardHeaderConfig } from './BoardHeader';
7 |
8 | import Panel from './Panel';
9 | import { TPanelStyles } from './Panel';
10 |
11 | import { TPositionTreeSetter, usePositionContext } from "../contexts/PositionContext";
12 | import { Key, useEffect } from "react";
13 | import Moves, { TMovesStyles } from "./Moves";
14 |
15 | export interface TAnalysisBoardStyles {
16 | analysisBoardContainerClassName: string
17 | }
18 |
19 | interface TProps {
20 | pgnString?: string,
21 | analysisPosition?: TPositionTreeSetter,
22 | getAnalysisPosition?: Function,
23 | config?: {
24 | boardHeaderConfig?: TBoardHeaderConfig,
25 | boardConfig?: TBoardConfig
26 | },
27 | styles?: {
28 | analysisBoardStyles?: TAnalysisBoardStyles,
29 | panelStyles?: TPanelStyles,
30 | movesStyles?: TMovesStyles,
31 | boardHeaderStyles?: TBoardHeaderStyles
32 | }
33 | }
34 |
35 | const AnalysisBoard = (props: TProps) => {
36 | const {
37 | pgnString,
38 | config,
39 | styles,
40 | analysisPosition,
41 | getAnalysisPosition,
42 | } = props
43 |
44 | const analysisBoardContainerClassName = styles?.analysisBoardStyles?.analysisBoardContainerClassName ?? 'RCAB-analysis-board-container'
45 |
46 |
47 | const { boardPosition, chessRootNode, handleLeftClick, handleRightClick, fen, chessNodes, setPosition } = usePositionContext()
48 |
49 | useEffect(() => {
50 | if (getAnalysisPosition && analysisPosition?.fen !== fen) {
51 | const tempNodes = chessNodes
52 | const newTempNodes = tempNodes?.map((el) => {
53 | const historyArray = el?.node?.history()
54 | const newEl = {
55 | ...el,
56 | historyArray
57 | }
58 | return newEl
59 | })
60 | const position = {
61 | pgnString,
62 | boardPosition,
63 | fen,
64 | chessNodes: newTempNodes
65 | }
66 | getAnalysisPosition(position)
67 | }
68 | }, [boardPosition, fen])
69 |
70 | useEffect(() => {
71 | if (analysisPosition && analysisPosition?.fen !== 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') {
72 | setPosition(analysisPosition)
73 | chessRootNode?.loadPgn(analysisPosition.pgnString)
74 | }
75 | else if(pgnString && chessRootNode) {
76 | chessRootNode.loadPgn(pgnString)
77 | }
78 | }, [analysisPosition, pgnString])
79 |
80 |
81 | const handleKeyDown = (k: Key) => {
82 | if (k === 'ArrowRight') {
83 | handleRightClick()
84 | }
85 | if (k === 'ArrowLeft') {
86 | handleLeftClick()
87 | }
88 | }
89 |
90 | return (
91 | handleKeyDown(k.key)} tabIndex={0}>
92 |
104 |
105 |
108 |
109 |
110 | )
111 | }
112 |
113 |
114 |
115 | export default AnalysisBoard
--------------------------------------------------------------------------------
/src/lib/components/Board.tsx:
--------------------------------------------------------------------------------
1 | import { Chessboard,
2 | ChessBoardProps as TChessBoardProps,
3 | CurrentPosition as TCurrentPosition,
4 | Pieces,
5 | Square} from "react-chessboard";
6 | import { Chess } from 'chess.js'
7 | import { usePositionContext } from "../contexts/PositionContext";
8 | import { useEffect, useState } from "react";
9 |
10 | export interface TBoardConfig {
11 | fen?: string
12 | ChessBoardProps?: TChessBoardProps
13 | }
14 |
15 | interface TProps {
16 | boardConfig?: TBoardConfig
17 | }
18 |
19 | const Board = (props: TProps) => {
20 | const { boardPosition, setBoardPosition,
21 | chessRootNode, chessNodes, setChessNodes,
22 | handleRightClick,
23 | fen, setFen
24 | } = usePositionContext();
25 | if (!chessRootNode || !chessNodes || !boardPosition) {
26 | return <>>
27 | }
28 |
29 | const {
30 | boardConfig = {}
31 | } = props
32 | if (!boardConfig.ChessBoardProps) {
33 | boardConfig.ChessBoardProps = {}
34 | }
35 |
36 | const [chessBoardProps, setChessBoardProps] = useState({...boardConfig.ChessBoardProps, position: fen})
37 |
38 | const handleGetcurrentPosition = (currentPosition: TCurrentPosition) => {
39 | if (callerGetPositionObject) {
40 | callerGetPositionObject(currentPosition)
41 | }
42 | }
43 | const handleOnPieceDrop = (sourceSquare: Square, targetSquare: Square, piece: Pieces) => {
44 | return onDrop(sourceSquare, targetSquare, piece)
45 | }
46 |
47 | const callerGetPositionObject = chessBoardProps.getPositionObject
48 |
49 | useEffect(() => {
50 | const tempChessRender = new Chess(fen)
51 | const currentNodeId = boardPosition?.nodeId
52 | const currentNode = chessNodes.filter(el => el.nodeId === currentNodeId)[0] ?? chessRootNode
53 | const currentNodeHistory = currentNode.node.history()
54 | currentNodeHistory.map((el, i) => {
55 | if (i < boardPosition?.moveIndex) {
56 | tempChessRender.move(el)
57 | }
58 | })
59 | const newFen = tempChessRender.fen()
60 | setFen(newFen)
61 | setChessBoardProps({
62 | ...chessBoardProps,
63 | getPositionObject: (currentPosition) => handleGetcurrentPosition(currentPosition),
64 | onPieceDrop: (sourceSquare, targetSquare, piece) => handleOnPieceDrop(sourceSquare, targetSquare, piece),
65 | position: newFen
66 | })
67 | }, [chessNodes])
68 |
69 | useEffect(() => {
70 | setChessBoardProps({
71 | ...chessBoardProps,
72 | getPositionObject: (currentPosition) => handleGetcurrentPosition(currentPosition),
73 | onPieceDrop: (sourceSquare, targetSquare, piece) => handleOnPieceDrop(sourceSquare, targetSquare, piece),
74 | position: fen
75 | })
76 | }, [fen])
77 |
78 | function makeAMove(move: {from: Square, to: Square, promotion?: string}, startingFen?: string) {
79 | const boardCopy = new Chess(startingFen);
80 | const newMove = boardCopy.move(move);
81 | if (!newMove) {
82 | return null
83 | }
84 | const newFen = boardCopy.fen()
85 | const result = {
86 | newFen,
87 | newMove
88 | }
89 | return result
90 | }
91 |
92 |
93 | function onDrop(sourceSquare: Square, targetSquare: Square, piece: Pieces) {
94 | let droppedMove
95 | if (targetSquare[1] === '8' && piece[1] === 'P') {
96 | droppedMove = {
97 | from: sourceSquare,
98 | to: targetSquare,
99 | promotion: 'q' // TODO: react-chessboard has promotion selection on their roadmap
100 | }
101 | } else {
102 | droppedMove = {
103 | from: sourceSquare,
104 | to: targetSquare
105 | }
106 | }
107 |
108 | const nextPosition = makeAMove(droppedMove, fen);
109 | const newFen = nextPosition?.newFen
110 | const newMove = nextPosition?.newMove
111 |
112 | // illegal move
113 | if (!newFen) {
114 | return false
115 | }
116 | if (chessNodes) {
117 | const currentNode = chessNodes.filter(el => el.nodeId === boardPosition.nodeId)[0] ?? chessRootNode
118 | const currentNodeCopy = currentNode.node
119 | const currentNodeHistory = currentNode.node.history()
120 | const nextMove = currentNodeHistory[boardPosition?.moveIndex]
121 | if (nextMove === newMove?.san) {
122 | handleRightClick()
123 | } else {
124 | const newNodeHistory = currentNodeHistory.slice(0, boardPosition?.moveIndex)
125 | if (newMove?.san) {
126 | if (newNodeHistory.length === currentNodeHistory.length) {
127 | currentNodeHistory.push(newMove?.san)
128 | currentNodeCopy.move(newMove?.san)
129 | const newChessNodes = chessNodes
130 | const replacementNode = {
131 | edgeNodeIndex: currentNode?.edgeNodeIndex,
132 | node: currentNodeCopy,
133 | nodeId: currentNode.nodeId,
134 | parentNodeId: currentNode.parentNodeId
135 | }
136 | newChessNodes[boardPosition.nodeId] = replacementNode
137 | const newMoveIndex = boardPosition.moveIndex + 1
138 | setBoardPosition({
139 | ...boardPosition,
140 | moveIndex: newMoveIndex
141 | })
142 | }
143 | else {
144 | newNodeHistory.push(newMove?.san)
145 | const newNode = new Chess(boardConfig.fen)
146 | newNodeHistory.map(el => newNode.move(el))
147 | const newNodeId = Math.max(...chessNodes.map(el => el.nodeId)) + 1
148 | const newChessNode = {
149 | edgeNodeIndex: boardPosition?.moveIndex,
150 | node: newNode,
151 | nodeId: newNodeId,
152 | parentNodeId: currentNode.nodeId
153 | }
154 | const newChessNodesCopy = chessNodes
155 | newChessNodesCopy.push(newChessNode)
156 | setChessNodes(newChessNodesCopy)
157 | const newMoveIndex = boardPosition.moveIndex + 1
158 | setBoardPosition({
159 | ...boardPosition,
160 | moveIndex: newMoveIndex,
161 | nodeId: newNodeId
162 | })
163 | }
164 | }
165 | }
166 | }
167 | setFen(newFen)
168 | return true;
169 | }
170 |
171 | return (
172 |
173 |
178 |
179 | )
180 |
181 | }
182 |
183 | export default Board
--------------------------------------------------------------------------------
/src/lib/components/BoardHeader.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { usePositionContext } from '../contexts/PositionContext'
3 | export interface TBoardHeaderDetails {
4 | White?: string,
5 | Black?: string,
6 | WhiteElo?: string,
7 | BlackElo?: string,
8 | Round?: string,
9 | Result?: string,
10 | Site?: string,
11 | Event?: string
12 | }
13 | export interface TBoardHeaderStyles {
14 | boardHeaderContainerClassName?: string
15 | boardHeaderTextClassName?: string
16 | boardHeaderTextDetailClassName?: string
17 | }
18 |
19 | export interface TBoardHeaderConfig {
20 | useHoodieGuyWhenUnknown?: boolean
21 | }
22 |
23 | interface TProps {
24 | boardHeaderStyles?: TBoardHeaderStyles,
25 | boardHeaderConfig?: TBoardHeaderConfig
26 | }
27 |
28 | const BoardHeader = (props: TProps) => {
29 | const {
30 | boardHeaderConfig = {},
31 | boardHeaderStyles = {},
32 | } = props
33 | const boardHeaderContainerClassName = boardHeaderStyles?.boardHeaderContainerClassName ?? 'RCAB-board-header-container'
34 | const boardHeaderTextClassName = boardHeaderStyles?.boardHeaderTextClassName ?? 'RCAB-board-header-text'
35 | const boardHeaderTextDetailClassName = boardHeaderStyles?.boardHeaderTextDetailClassName ?? 'RCAB-board-header-text-detail'
36 |
37 |
38 | const { chessRootNode } = usePositionContext()
39 | if (!chessRootNode) {
40 | return <>>
41 | }
42 |
43 | const boardHeaderDetails: TBoardHeaderDetails = chessRootNode.header()
44 | if (!boardHeaderDetails) {
45 | return <>>
46 | }
47 |
48 |
49 | const {
50 | useHoodieGuyWhenUnknown = false
51 | } = boardHeaderConfig
52 | const nameDefault = useHoodieGuyWhenUnknown
53 | ? 'Mr. Hoodie Guy'
54 | : 'Unknown'
55 |
56 | const {
57 | White = nameDefault, Black = nameDefault,
58 | WhiteElo, BlackElo,
59 | Round, Result, Event,
60 | Site
61 | } = boardHeaderDetails
62 |
63 | return (
64 |
65 |
66 | {White} {WhiteElo && WhiteElo.length > 0 && WhiteElo !== '?' ? `(${WhiteElo})` : ''}
67 | vs.{` `}
68 | {Black} {BlackElo && BlackElo.length > 0 && BlackElo !== '?' ? `(${BlackElo})` : ''}
69 |
70 | {Event && Event.length > 0 ? `${Event} | ` : ''}
71 | {Site && Site.length > 0 ? `${Site} | ` : ''}
72 | {Round && Round.length > 0 && Round !== '?' ? `${Round} | ` : ''}
73 | {Result ? Result : ''}
74 |
75 |
76 | )
77 | }
78 |
79 | export default BoardHeader
--------------------------------------------------------------------------------
/src/lib/components/Moves.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { usePositionContext } from "../contexts/PositionContext"
3 |
4 | export interface TMovesStyles {
5 | movesContainerClassName?: string
6 | movesClassName?: string
7 | }
8 |
9 | interface TProps {
10 | movesStyles?: TMovesStyles
11 | }
12 |
13 | interface TMove {
14 | index: number,
15 | moveNumber: number,
16 | move: string,
17 | nodeId: number,
18 | parentNodeId?: number,
19 | edgeNodeIndex: number,
20 | className?: string
21 | }
22 |
23 | export type TNodeMoves = TMove[]
24 |
25 | export type TMoves = TNodeMoves[]
26 |
27 | const Moves = (props: TProps) => {
28 | const {
29 | movesStyles
30 | } = props
31 |
32 | const movesContainerClassName = movesStyles?.movesContainerClassName ?? 'RCAB-moves-container'
33 |
34 | const { chessNodes, boardPosition, setBoardPosition } = usePositionContext()
35 | if (!chessNodes) {
36 | return <>>
37 | }
38 |
39 | const handleSelectMove = (move: TMove) => {
40 | setBoardPosition({
41 | ...boardPosition,
42 | moveIndex: move.index + 1,
43 | nodeId: move.nodeId
44 | })
45 | }
46 |
47 | const [renderMoves, setRenderMoves] = useState([])
48 |
49 | useEffect(() => {
50 | const moves = chessNodes.map(el => {
51 | const history = el.node.history()
52 | const nodeMoves = history.map((move, i) => {
53 | if (typeof(move) !== 'string') {
54 | move = move.san
55 | }
56 | const moveNumber = Math.floor(i / 2) + 1
57 | let className = 'RCAB-'
58 | if (el.nodeId === 0) {
59 | className += 'root-'
60 | }
61 | if (boardPosition.nodeId === el.nodeId) {
62 | className += 'active-node'
63 | if (i === el?.edgeNodeIndex) {
64 | className += '-first'
65 | }
66 | if (i === history.length - 1 && i !== 0) {
67 | className += '-last'
68 | }
69 | if (boardPosition.moveIndex === i + 1) {
70 | className += '-selected'
71 | }
72 | } else {
73 | className += 'inactive-node'
74 | if (i === el?.edgeNodeIndex) {
75 | className += '-first'
76 | }
77 | if (i === history.length - 1 && i !== 0) {
78 | className += '-last'
79 | }
80 | }
81 | return {
82 | index: i,
83 | moveNumber,
84 | move,
85 | nodeId: el?.nodeId,
86 | parentNodeId: el?.parentNodeId,
87 | edgeNodeIndex: el?.edgeNodeIndex,
88 | className
89 | }
90 | })
91 | return nodeMoves
92 | })
93 | if (moves.length > 1) {
94 | moves.sort((a, b) => a[0]?.parentNodeId - b[0]?.parentNodeId)
95 | }
96 | const compressMoves = (moves: TMoves) => {
97 | const movesCopy = moves
98 | if (movesCopy.length === 1) {
99 | moves = movesCopy
100 | return
101 | }
102 | const child = movesCopy[movesCopy.length - 1]
103 | const parent = movesCopy.filter(el => el[0]?.nodeId === child[0]?.parentNodeId)[0]
104 | const parentIndex = movesCopy.indexOf(parent)
105 | const parentCopy = parent
106 | for (let i = child[0]?.edgeNodeIndex; i < child.length; i++) {
107 | parentCopy?.splice(i, 0, child[i])
108 | }
109 | movesCopy[parentIndex] = parentCopy
110 | movesCopy?.pop()
111 | compressMoves(movesCopy)
112 | }
113 | compressMoves(moves)
114 | setRenderMoves(moves[0])
115 | }, [chessNodes, boardPosition])
116 |
117 | return (
118 |
119 | {renderMoves.map((move: TMove) => {
120 | const moveNumber = move.index % 2 === 0 ? move.moveNumber + '.' : ''
121 | if (move.className?.includes('first') && move?.parentNodeId === 0) {
122 | return (
123 |
124 |
125 | handleSelectMove(move)}
127 | className={move.className}
128 | >
129 | {`${moveNumber} ${move.move} `}
130 |
131 |
132 | )
133 | }
134 | if (move.className?.includes('last') && (move.nodeId !== 0)) {
135 | return (
136 |
137 | handleSelectMove(move)}
139 | className={move.className}
140 | >
141 | {`${moveNumber} ${move.move} `}
142 |
143 |
144 |
145 | )
146 | }
147 | return (
148 |
handleSelectMove(move)}
150 | className={move.className}
151 | key={`${move.nodeId}${move.index}`}
152 | >
153 | {`${moveNumber} ${move.move} `}
154 |
155 | )
156 | })}
157 |
158 | )
159 | }
160 |
161 | export default Moves
--------------------------------------------------------------------------------
/src/lib/components/Panel.tsx:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2 | import { faArrowLeftLong } from '@fortawesome/free-solid-svg-icons'
3 | import { faArrowRightLong } from '@fortawesome/free-solid-svg-icons'
4 | import { usePositionContext } from '../contexts/PositionContext'
5 |
6 |
7 | export interface TPanelStyles {
8 | panelContainerClassName?: string
9 | panelClassName?: string
10 | }
11 |
12 | interface TProps {
13 | panelStyles?: TPanelStyles
14 | }
15 | const Panel = (props: TProps) => {
16 | const {
17 | panelStyles
18 | } = props
19 |
20 | const panelContainerClassName = panelStyles?.panelContainerClassName ?? 'RCAB-panel-container'
21 | // const panelClassName = panelStyles?.panelClassName ?? 'RCAB-panel'
22 |
23 | const { chessRootNode, chessNodes, handleLeftClick, handleRightClick } = usePositionContext()
24 | if (!chessRootNode || !chessNodes) {
25 | return <>>
26 | }
27 |
28 | return (
29 |
30 |
handleLeftClick()}>
31 |
32 |
33 |
34 |
handleRightClick()}>
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default Panel
--------------------------------------------------------------------------------
/src/lib/contexts/PositionContext.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext, createContext, useEffect } from 'react';
2 | import { Chess, Move } from 'chess.js'
3 |
4 | const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
5 |
6 | interface TChessNodes {
7 | edgeNodeIndex: number,
8 | node: Chess,
9 | nodeId: number,
10 | parentNodeId: number,
11 | historyArray?: string | Move[]
12 | }
13 |
14 | interface TBoardPosition {
15 | nodeId: number,
16 | moveIndex: number
17 | }
18 |
19 | interface TPositionTree {
20 | boardPosition: TBoardPosition,
21 | setBoardPosition: Function,
22 | fen?: string,
23 | chessRootNode?: Chess,
24 | chessNodes?: TChessNodes[],
25 | handleRightClick: Function,
26 | handleLeftClick: Function,
27 | setChessNodes: Function,
28 | setFen: Function,
29 | getPosition?: Function,
30 | setPosition: Function
31 | }
32 |
33 | export interface TPositionTreeSetter {
34 | pgnString: string,
35 | boardPosition: TBoardPosition,
36 | fen?: string,
37 | chessNodes: TChessNodes[],
38 | }
39 |
40 | const PositionContext = createContext({
41 | boardPosition: {
42 | nodeId: 0,
43 | moveIndex: 0
44 | },
45 | setBoardPosition: () => {},
46 | handleRightClick: () => {},
47 | handleLeftClick: () => {},
48 | setChessNodes: () => {},
49 | setFen: () => {},
50 | setPosition: () => {}
51 | });
52 |
53 | export const PositionContextProvider = (props: any) => {
54 |
55 | const { initialFen } = props
56 |
57 | const [boardPosition, setBoardPosition] = useState({
58 | nodeId: 0,
59 | moveIndex: 0
60 | })
61 | const [chessRootNode] = useState(new Chess(initialFen ?? START_FEN))
62 | const [chessNodes, setChessNodes] = useState([{
63 | edgeNodeIndex: 0,
64 | node: chessRootNode,
65 | nodeId: 0,
66 | parentNodeId: -1
67 | }])
68 |
69 | const [fen, setFen] = useState(initialFen ?? START_FEN)
70 | useEffect(() => {
71 | const currentNode = chessNodes.filter((el) => el.nodeId === boardPosition.nodeId)[0]
72 | const currentNodeHistory = currentNode.node.history()
73 | const tempChessRender = new Chess(initialFen ?? START_FEN)
74 | currentNodeHistory.map((el, i) => {
75 | if (i < boardPosition?.moveIndex) {
76 | tempChessRender.move(el)
77 | }
78 | })
79 | const newFen = tempChessRender.fen()
80 | setFen(newFen)
81 |
82 | }, [boardPosition])
83 |
84 | const handleRightClick = () => {
85 | const currentNode = chessNodes.filter(el => el.nodeId === boardPosition.nodeId)[0]
86 | const boardHistory = currentNode.node.history()
87 | if (boardPosition?.moveIndex < boardHistory.length) {
88 | const newMoveIndex = boardPosition.moveIndex + 1
89 | setBoardPosition({
90 | ...boardPosition,
91 | moveIndex: newMoveIndex
92 | })
93 | currentNode.node.move(boardHistory[newMoveIndex])
94 | }
95 | }
96 |
97 | const handleLeftClick = () => {
98 | if (boardPosition.nodeId === 0 && boardPosition.moveIndex === 0) {
99 | return
100 | }
101 | if (boardPosition.nodeId === 0 && boardPosition.moveIndex === 1) {
102 | setBoardPosition({
103 | ...boardPosition,
104 | moveIndex: 0
105 | })
106 | return
107 | }
108 | const currentNode = chessNodes.filter(el => el.nodeId === boardPosition.nodeId)[0]
109 | const edgeNodeIndex = currentNode?.edgeNodeIndex
110 | const newMoveIndex = boardPosition.moveIndex - 1
111 | if (newMoveIndex > edgeNodeIndex) {
112 | setBoardPosition({
113 | ...boardPosition,
114 | moveIndex: newMoveIndex
115 | })
116 | } else {
117 | setBoardPosition({
118 | ...boardPosition,
119 | moveIndex: newMoveIndex,
120 | nodeId: currentNode.parentNodeId
121 | })
122 | }
123 | }
124 |
125 | const setPosition = (newPosition: TPositionTreeSetter) => {
126 | setBoardPosition(newPosition.boardPosition)
127 | if (newPosition.fen) {
128 | setFen(newPosition.fen)
129 | }
130 | const tempNodes = newPosition.chessNodes
131 | const newTempNodes = tempNodes.map((el) => {
132 | const tempNode = new Chess()
133 | if (typeof el?.historyArray !== 'string') {
134 | el?.historyArray?.map((move) => {
135 | tempNode.move(move)
136 | })
137 | }
138 | const newEl = {
139 | ...el,
140 | node: tempNode
141 | }
142 | return newEl
143 | })
144 | setChessNodes(newTempNodes)
145 | }
146 |
147 | return {props.children}
153 | }
154 |
155 | export const usePositionContext = () => useContext(PositionContext);
156 |
157 |
158 | function findTransitionMove(fen1: string, fen2: string) {
159 | const chess = new Chess(fen1);
160 | const moves = chess.moves({ verbose: true });
161 |
162 | for (const move of moves) {
163 | chess.move(move);
164 | if (chess.fen() === fen2) {
165 | chess.undo();
166 | return move;
167 | }
168 | chess.undo();
169 | }
170 |
171 | return null;
172 | }
--------------------------------------------------------------------------------
/src/lib/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import { describe, expect, test } from 'vitest'
4 | import PGNBoard from './index'
5 |
6 | describe('PGNBoard', () => {
7 | test('PGNBoard component renders correctly', () => {
8 | const component = renderer.create(
9 |
10 | )
11 |
12 | const tree = component.toJSON()
13 |
14 | expect(tree).toMatchSnapshot()
15 | })
16 |
17 | test('The greetee prop works', () => {
18 | const component = renderer.create(
19 |
20 | )
21 |
22 | const tree = component.toJSON()
23 |
24 | expect(tree).toMatchSnapshot()
25 | })
26 | })
--------------------------------------------------------------------------------
/src/lib/index.tsx:
--------------------------------------------------------------------------------
1 | import './style.scss'
2 |
3 | import { PositionContextProvider, TPositionTreeSetter } from './contexts/PositionContext';
4 | import { TPanelStyles } from './components/Panel';
5 | import { TBoardHeaderConfig, TBoardHeaderStyles } from './components/BoardHeader';
6 | import { TBoardConfig } from './components/Board';
7 |
8 | import AnalysisBoard , { TAnalysisBoardStyles } from './components/AnalysisBoard';
9 | import { TMovesStyles } from './components/Moves';
10 |
11 | export interface TProps {
12 | pgnString?: string
13 | config?: {
14 | boardHeaderConfig?: TBoardHeaderConfig,
15 | boardConfig?: TBoardConfig
16 | },
17 | getAnalysisPosition?: Function,
18 | analysisPosition?: TPositionTreeSetter,
19 | styles?: {
20 | analysisBoardStyles?: TAnalysisBoardStyles,
21 | panelStyles?: TPanelStyles,
22 | movesStyles?: TMovesStyles,
23 | boardHeaderStyles?: TBoardHeaderStyles
24 | }
25 | }
26 |
27 | const ChessAnalysisBoard = (props: TProps) => {
28 |
29 | const { config } = props
30 |
31 | return (
32 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default ChessAnalysisBoard
41 |
--------------------------------------------------------------------------------
/src/lib/style.scss:
--------------------------------------------------------------------------------
1 | .RCAB-board-header-container {
2 | padding: 5px;
3 | }
4 |
5 | .RCAB-board-header-text {
6 | font-weight: bold;
7 | margin-bottom: .25rem;
8 | }
9 |
10 | .RCAB-board-header-text-detail {
11 | font-size: 14px;
12 | }
13 |
14 | .RCAB-panel-container {
15 | width: 560px;
16 | display: flex;
17 | border-style: solid;
18 | align-items: center;
19 | justify-content: space-evenly;
20 | }
21 |
22 | .RCAB-analysis-board-container {
23 | outline: none;
24 | display: flex;
25 | flex-direction: row;
26 | }
27 |
28 | .RCAB-moves-container {
29 | padding: 48px;
30 | width: 24vw;
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from "react-dom/client";
3 | import './index.scss'
4 | import App from './App'
5 |
6 | const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
7 | root.render(
8 |
9 |
10 | ,
11 | )
--------------------------------------------------------------------------------
/src/style.scss:
--------------------------------------------------------------------------------
1 | .panel-container {
2 | width: 560px;
3 | display: flex;
4 | border-style: solid;
5 | align-items: center;
6 | justify-content: space-evenly;
7 | border-color: gray;
8 | }
9 |
10 | .board-header-container {
11 | padding: 8px
12 | }
13 |
14 | .board-header-text {
15 | font-weight: bold;
16 | margin-bottom: .24rem;
17 | }
18 |
19 |
20 | .board-header-text-detail {
21 | font-size: 16px;
22 | }
23 |
24 | .RCAB-board-header-container {
25 | padding: 8px;
26 | }
27 |
28 | .RCAB-board-header-text {
29 | font-weight: bold;
30 | margin-bottom: .24rem;
31 | }
32 |
33 | .RCAB-board-header-text-detail {
34 | font-size: 16px;
35 | }
36 |
37 | .RCAB-panel-container {
38 | width: 560px;
39 | display: flex;
40 | border-style: solid;
41 | align-items: center;
42 | justify-content: space-evenly;
43 | border-color: gray;
44 | }
45 |
46 | .RCAB-analysis-board-container {
47 | outline: none;
48 | display: flex;
49 | flex-direction: row;
50 | }
51 |
52 | /*
53 | ************
54 | Moves
55 | ************
56 | */
57 |
58 | .RCAB-moves-container {
59 | padding: 48px;
60 | width: 24vw;
61 | }
62 |
63 | /* Root */
64 | .RCAB-root-active-node {
65 | cursor: pointer;
66 | color: black;
67 | }
68 |
69 | .RCAB-root-active-node-selected {
70 | cursor: pointer;
71 | color: green;
72 | }
73 | .RCAB-root-active-node-first {
74 | cursor: pointer;
75 | color: black;
76 | }
77 | .RCAB-root-active-node-last {
78 | cursor: pointer;
79 | color: black;
80 | }
81 | .RCAB-root-active-node-first-selected {
82 | cursor: pointer;
83 | color: green;
84 | }
85 | .RCAB-root-active-node-last-selected {
86 | cursor: pointer;
87 | color: green;
88 | }
89 |
90 | .RCAB-root-inactive-node-first {
91 | cursor: pointer;
92 | color: black;
93 | }
94 | .RCAB-root-inactive-node {
95 | cursor: pointer;
96 | color: black;
97 | }
98 | .RCAB-root-inactive-node-last {
99 | cursor: pointer;
100 | color: black;
101 | }
102 |
103 | .RCAB-inactive-node {
104 | cursor: pointer;
105 | color: gray;
106 | }
107 |
108 | .RCAB-inactive-node-first {
109 | cursor: pointer;
110 | color: gray;
111 | }
112 | .RCAB-inactive-node-last {
113 | cursor: pointer;
114 | color: gray;
115 | }
116 | .RCAB-inactive-node-first-last {
117 | cursor: pointer;
118 | color: gray;
119 | }
120 | .RCAB-active-node {
121 | cursor: pointer;
122 | color: blue;
123 | }
124 | .RCAB-active-node-first {
125 | cursor: pointer;
126 | color: blue;
127 | }
128 | .RCAB-active-node-first-selected {
129 | cursor: pointer;
130 | color: green;
131 | }
132 | .RCAB-active-node-last {
133 | cursor: pointer;
134 | color: blue;
135 | }
136 | .RCAB-active-node-last-selected {
137 | cursor: pointer;
138 | color: green;
139 | }
140 | .RCAB-active-node-first-last {
141 | cursor: pointer;
142 | color: blue;
143 | }
144 | .RCAB-active-node-first-last-selected {
145 | cursor: pointer;
146 | color: green;
147 | }
148 | .RCAB-active-node-selected {
149 | cursor: pointer;
150 | color: green;
151 | }
152 | .RCAB-move-separator {
153 | border-top: 1px solid gray;
154 | }
155 | .RCAB-submove-separator {
156 | border-top: 2px solid gray;
157 | }
158 |
159 |
160 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { defineConfig } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import dts from 'vite-plugin-dts';
5 | // import typescript from '@rollup/plugin-typescript';
6 |
7 | export default defineConfig({
8 | build: {
9 | lib: {
10 | entry: path.resolve(__dirname, 'src/lib/index.tsx'),
11 | name: 'ChessAnalysisBoard',
12 | fileName: `react-chess-analysis-board`
13 | },
14 | rollupOptions: {
15 | external: ['react', 'react-dom'],
16 | output: {
17 | globals: {
18 | react: 'React',
19 | },
20 | sourcemapExcludeSources: true,
21 | }
22 | },
23 | sourcemap: true,
24 | target: 'esnext',
25 | minify: false
26 | },
27 | plugins: [
28 | react(),
29 | dts({ insertTypesEntry: true, }),
30 | // only for type checking
31 | // typescript()
32 | ]
33 | })
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite'
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: false,
7 | environment: 'jsdom'
8 | }
9 | })
--------------------------------------------------------------------------------