├── .gitignore ├── README.md ├── chess_api.py ├── package-lock.json ├── package.json ├── public ├── cbi_logo.PNG ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── app │ ├── ccm │ │ └── ccm.js │ ├── evalbars │ │ ├── App.css │ │ ├── App.js │ │ └── App.test.js │ ├── index.js │ ├── landing-page │ │ ├── LandingPage.css │ │ └── LandingPage.js │ └── message-display │ │ └── messagedisplay.js ├── assets │ └── blunder-sound.mp3 ├── components │ ├── customize-evalbar │ │ ├── CustomizeEvalBar.css │ │ └── CustomizeEvalBar.js │ ├── evalbar │ │ ├── EvalBar.css │ │ └── Evalbar.js │ ├── index.js │ └── tournaments-list │ │ └── TournamentsList.js ├── index.css ├── index.js └── logo.svg ├── start.sh └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChessBase India Broadcast Feature 2 | 3 | This is a React-based application that allows broadcasters to display multiple evaluation bars for different chess games happening simultaneously. The project was created using \`create-react-app\`. 4 | 5 | ## Features 6 | 7 | - **Multiple Game Displays**: The broadcaster can display evaluation bars for multiple chess games at the same time, allowing viewers to keep track of the progress and positions of multiple games. 8 | - **Real-time Updates**: The evaluation bars update in real-time, providing viewers with a dynamic and up-to-date view of the games. 9 | - **Customizable Layouts**: The broadcaster can customize the layout of the evaluation bars, adjusting their size, position, and arrangement to suit their preferences. 10 | - **Responsive Design**: The application is designed to be responsive, ensuring a great user experience on various devices and screen sizes. 11 | 12 | ## Getting Started 13 | 14 | To get started with the project, follow these steps: 15 | 16 | 1. **Clone the Repository**: 17 | ```bash 18 | git clone https://github.com/Chess-broadcasting-tools/eval-bar.git 19 | ``` 20 | 21 | 2. **Install Dependencies**: 22 | ```bash 23 | cd eval-bar 24 | npm install 25 | ``` 26 | 27 | 3. **Start the Development Server**: 28 | ```bash 29 | npm start 30 | ``` 31 | 32 | This will start the development server and open the application in your default web browser. 33 | 34 | ## Usage 35 | 36 | 1. **Use the deployed Backend or serve your own stockfish**: As of when this readme was written the backend(stockfish served via flask) is "https://stockfish.broadcastsofcbi.live/evaluate?fen={fen}" example usage below . To use this backend you don't have to do anything , the API call is being made through App.js , and the endpoint is mentioned. 37 | ```bash 38 | https://stockfish.broadcastsofcbi.live/evaluate?fen=2r2r1k/pp4pp/8/1N1p1PR1/P1Bp4/3P3q/1PP2P1N/3R3K%20w%20-%20-%200%2022 39 | ``` 40 | 2.**Deploy to Production**: When you're ready to deploy the application to production, use the following command: 41 | ```bash 42 | npm run build 43 | ``` 44 | This will create an optimized production build that you can deploy to your hosting platform. 45 | 46 | 3. **Select Tournament**: To display the games , use the /evalbars route, you should be able to see ongoing tournaments, if you see no tournaments that means no tournaments are going on . To still use and test it, you can use a old lichess broadcast link in the custom url input box. You can select the desired tournament/tournaments and then click on Confirm button , and select the required bars and then click "Add selected games bar" 47 | 48 | 4. **Customize Layout**: Adjust the layout of the evaluation bars by modifying the customize the bars button. 49 | 50 | 51 | This will create an optimized production build that you can deploy to your hosting platform. 52 | 53 | ## Contributing 54 | 55 | If you find any issues or have suggestions for improvements, feel free to open an issue or submit a pull request. Contributions are always welcome! 56 | 57 | ## License 58 | 59 | This project is licensed under the [MIT License](LICENSE). 60 | -------------------------------------------------------------------------------- /chess_api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, send_from_directory 2 | from flask_cors import CORS 3 | from stockfish import Stockfish 4 | import os 5 | 6 | app = Flask(__name__, static_folder='./build') 7 | CORS(app) 8 | 9 | current_os = os.name 10 | if current_os == 'nt': 11 | stockfish_path = os.path.join(os.path.dirname(__file__), "stockfish_binaries", "stockfish-windows-x86-64-avx2.exe") 12 | elif current_os == 'posix': 13 | stockfish_path = "//opt/homebrew/bin/stockfish" 14 | else: 15 | raise Exception("Unsupported OS") 16 | 17 | stockfish = Stockfish(stockfish_path) 18 | stockfish.set_depth(10) 19 | 20 | @app.route('/evaluate', methods=['GET']) 21 | def evaluate(): 22 | fen = request.args.get('fen') 23 | 24 | if not fen: 25 | return jsonify(error="FEN string not provided"), 400 26 | 27 | stockfish.set_fen_position(fen) 28 | evaluation = stockfish.get_evaluation() 29 | 30 | if evaluation["type"] == "cp": 31 | score = evaluation["value"] / 100.0 32 | elif evaluation["type"] == "mate": 33 | score = 9999 * evaluation["value"] / abs(evaluation["value"]) 34 | 35 | score = score * 1.2 36 | score = round(score, 1) 37 | 38 | return jsonify(evaluation=score) 39 | 40 | @app.route('/', defaults={'path': ''}) 41 | @app.route('/') 42 | def serve(path): 43 | if path != "" and os.path.exists("./build/" + path): 44 | return send_from_directory("./build", path) 45 | else: 46 | return send_from_directory("./build", 'index.html') 47 | 48 | if __name__ == '__main__': 49 | app.run(debug=True, port=5000) 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cbi-eval", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/react": "^2.8.2", 7 | "@emotion/react": "^11.11.1", 8 | "@emotion/styled": "^11.11.0", 9 | "@mui/icons-material": "^5.14.1", 10 | "@mui/lab": "^5.0.0-alpha.153", 11 | "@mui/material": "^5.14.19", 12 | "@testing-library/jest-dom": "^5.17.0", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "axios": "^1.4.0", 16 | "chess.js": "^1.0.0-beta.6", 17 | "chessboardjsx": "^2.4.7", 18 | "framer-motion": "^10.16.12", 19 | "mui-color-input": "^2.0.1", 20 | "pgn-parser": "^2.2.0", 21 | "react": "^18.2.0", 22 | "react-color": "^2.19.3", 23 | "react-dom": "^18.2.0", 24 | "react-router-dom": "^6.20.0", 25 | "react-scripts": "5.0.1", 26 | "styled-components": "^6.1.1", 27 | "web-vitals": "^2.1.4" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "tailwindcss": "^3.3.5", 55 | "@babel/plugin-proposal-private-property-in-object": "^7.14.5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/cbi_logo.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chess-broadcasting-tools/eval-bar/d9d2d69a87a2074680adea50a43a8ce2df9cca00/public/cbi_logo.PNG -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chess-broadcasting-tools/eval-bar/d9d2d69a87a2074680adea50a43a8ce2df9cca00/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | Chess Broadcasting Tools 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chess-broadcasting-tools/eval-bar/d9d2d69a87a2074680adea50a43a8ce2df9cca00/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chess-broadcasting-tools/eval-bar/d9d2d69a87a2074680adea50a43a8ce2df9cca00/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Chess Broadcasting Tools", 3 | "name": "Create React App Sample", 4 | "icons": [{ 5 | "src": "favicon.ico", 6 | "sizes": "64x64 32x32 24x24 16x16", 7 | "type": "image/x-icon" 8 | }, 9 | { 10 | "src": "logo192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "logo512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | } 19 | ], 20 | "start_url": ".", 21 | "display": "standalone", 22 | "theme_color": "#000000", 23 | "background_color": "#ffffff" 24 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/app/ccm/ccm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function ccm(){ 4 | return( 5 |

coming soon!

6 | ) 7 | } 8 | export default ccm; -------------------------------------------------------------------------------- /src/app/evalbars/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | --primary-font: 'Roboto', sans-serif; 3 | --transition-speed: 0.3s; 4 | --primary-color: rgb(21, 185, 81); 5 | background: rgb(0, 0, 0); 6 | --container-bg: rgba(13, 255, 0, 0.9); 7 | --border-color: #ccc; 8 | --focus-color: #30517c; 9 | --button-bg: #1a1a1a; 10 | --button-hover-transform: translateY(-2px); 11 | --box-shadow-light: 0px 3px 10px rgba(0, 0, 0, 0.1); 12 | --box-shadow-dark: 0px 5px 15px rgba(0, 0, 0, 0.2); 13 | } 14 | 15 | body, 16 | html { 17 | margin: 0; 18 | padding: 0; 19 | width: 100%; 20 | font-family: var(--primary-font); 21 | overflow: auto; 22 | } 23 | 24 | * { 25 | transition: all var(--transition-speed) ease; 26 | } 27 | 28 | body.chroma-background { 29 | background-color: var(--primary-color); 30 | } 31 | 32 | .text-field { 33 | border-radius: 5px; 34 | border: 1px solid var(--border-color); 35 | } 36 | 37 | .text-field:focus { 38 | border-color: var(--focus-color); 39 | box-shadow: 0px 0px 5px rgba(26, 115, 232, 0.4); 40 | } 41 | 42 | .button { 43 | border-radius: 20px; 44 | padding: 10px 20px; 45 | text-transform: uppercase; 46 | box-shadow: var(--box-shadow-light); 47 | transition: transform 0.2s ease; 48 | } 49 | 50 | .button:hover { 51 | transform: var(--button-hover-transform); 52 | } 53 | 54 | .eval-bars-container { 55 | width: 100%; 56 | padding-bottom: 30px; 57 | /* Adjust this value as needed */ 58 | box-sizing: border-box; 59 | /* Include padding in the element's total height/width */ 60 | } 61 | 62 | 63 | /* pulse animation */ 64 | 65 | @keyframes pulse { 66 | 0% { 67 | transform: scale(1); 68 | } 69 | 50% { 70 | transform: scale(1.1); 71 | } 72 | 100% { 73 | transform: scale(1); 74 | } 75 | } -------------------------------------------------------------------------------- /src/app/evalbars/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react"; 2 | import { Toolbar, Button, Container, Box } from "@mui/material"; 3 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 4 | import { Chess } from "chess.js"; 5 | import { EvalBar, TournamentsList, CustomizeEvalBar } from "../../components"; 6 | import "./App.css"; 7 | import blunderSound from "../../assets/blunder-sound.mp3"; 8 | 9 | const theme = createTheme({ 10 | palette: { 11 | mode: "dark", 12 | background: { 13 | default: "transparent", 14 | }, 15 | primary: { 16 | main: "#00008b", 17 | }, 18 | secondary: { 19 | main: "#b9bbce", 20 | }, 21 | tertiary: { 22 | main: "#ADD8E6", 23 | }, 24 | }, 25 | fontFamily: [ 26 | "-apple-system", 27 | "BlinkMacSystemFont", 28 | '"Segoe UI"', 29 | "Roboto", 30 | '"Helvetica Neue"', 31 | "Arial", 32 | "sans-serif", 33 | '"Apple Color Emoji"', 34 | '"Segoe UI Emoji"', 35 | '"Segoe UI Symbol"', 36 | ].join(","), 37 | }); 38 | 39 | const GameCard = ({ game, onClick, isSelected }) => { 40 | const variant = isSelected ? "contained" : "outlined"; 41 | const color = isSelected ? "tertiary" : "secondary"; 42 | const boxShadow = isSelected 43 | ? "0px 0px 12px 2px rgba(252,188,213,0.6)" 44 | : "none"; 45 | 46 | const buttonStyle = { 47 | margin: "2px", 48 | padding: "6px", 49 | fontSize: "0.8em", 50 | fontWeight: "bold", 51 | boxShadow, 52 | }; 53 | 54 | return ( 55 | 63 | ); 64 | }; 65 | 66 | function App() { 67 | const [broadcastIDs, setBroadcastIDs] = useState([]); 68 | const [isBroadcastLoaded, setIsBroadcastLoaded] = useState(false); 69 | const [links, setLinks] = useState([]); 70 | const [availableGames, setAvailableGames] = useState([]); 71 | const [selectedGames, setSelectedGames] = useState([]); 72 | const [blunderAlertLinks, setBlunderAlertLinks] = useState([]); 73 | const blunderSoundRef = useRef(null); 74 | const [customStyles, setCustomStyles] = useState({ 75 | evalContainerBg: "#000000", 76 | blackBarColor: "#E79D29", 77 | whiteBarColor: "#ffffff", 78 | whitePlayerColor: "Transparent", 79 | blackPlayerColor: "Transparent", 80 | whitePlayerNameColor: "#FFFFFF", 81 | blackPlayerNameColor: "#E79D29", 82 | evalContainerBorderColor: "#FFFFFF", 83 | }); 84 | 85 | const [layout, setLayout] = useState("grid"); 86 | const [isChromaBackground, setIsChromaBackground] = useState(true); 87 | 88 | const allGames = useRef(""); 89 | const abortControllers = useRef({}); 90 | 91 | const handleBlunder = (linkIndex) => { 92 | setBlunderAlertLinks((prevLinks) => [...prevLinks, linkIndex]); 93 | blunderSoundRef.current.play(); 94 | 95 | setTimeout(() => { 96 | setBlunderAlertLinks((prevLinks) => 97 | prevLinks.filter((index) => index !== linkIndex) 98 | ); 99 | }, 40000); 100 | }; 101 | const handleDemoBlunder = () => { 102 | if (links.length > 0) { 103 | const randomLinkIndex = Math.floor(Math.random() * links.length); 104 | handleBlunder(randomLinkIndex); 105 | } 106 | }; 107 | 108 | const fetchEvaluation = async (fen) => { 109 | const endpoint = `https://stockfish.broadcastsofcbi.live/evaluate?fen=${encodeURIComponent( 110 | fen 111 | )}`; 112 | const response = await fetch(endpoint, { method: "GET", mode: "cors" }); 113 | if (!response.ok) { 114 | throw new Error("Network response was not ok"); 115 | } 116 | return await response.json(); 117 | }; 118 | 119 | const handleRemoveLink = (index) => { 120 | setLinks((prevLinks) => prevLinks.filter((link, i) => i !== index)); 121 | }; 122 | 123 | const handleTournamentSelection = async (tournamentIds) => { 124 | console.log("Received Tournament IDs:", tournamentIds); 125 | setIsBroadcastLoaded(true); 126 | setIsChromaBackground(true); 127 | tournamentIds.forEach((tournamentId) => startStreaming(tournamentId)); 128 | }; 129 | 130 | const startStreaming = async (tournamentId) => { 131 | if (abortControllers.current[tournamentId]) 132 | abortControllers.current[tournamentId].abort(); 133 | abortControllers.current[tournamentId] = new AbortController(); 134 | 135 | const streamURL = `https://lichess.org/api/stream/broadcast/round/${tournamentId}.pgn`; 136 | const response = await fetch(streamURL, { 137 | signal: abortControllers.current[tournamentId].signal, 138 | }); 139 | console.log("Stream URL:", streamURL); 140 | const reader = response.body.getReader(); 141 | document.body.classList.add("chroma-background"); 142 | 143 | const processStream = async () => { 144 | const { done, value } = await reader.read(); 145 | if (done) return; 146 | 147 | allGames.current += new TextDecoder().decode(value); 148 | updateEvaluations(); 149 | fetchAvailableGames(); 150 | setTimeout(processStream, 10); 151 | }; 152 | processStream(); 153 | }; 154 | 155 | const fetchAvailableGames = () => { 156 | const games = allGames.current.split("\n\n\n"); 157 | const gameOptions = games 158 | .map((game) => { 159 | const whiteMatch = game.match(/\[White "(.*?)"\]/); 160 | const blackMatch = game.match(/\[Black "(.*?)"\]/); 161 | return whiteMatch && blackMatch 162 | ? `${whiteMatch[1]} - ${blackMatch[1]}` 163 | : null; 164 | }) 165 | .filter(Boolean); 166 | setAvailableGames(Array.from(new Set(gameOptions))); 167 | }; 168 | 169 | const handleGameSelection = (game) => { 170 | if (selectedGames.includes(game)) { 171 | setSelectedGames((prevGames) => prevGames.filter((g) => g !== game)); 172 | } else { 173 | setSelectedGames((prevGames) => [...prevGames, game]); 174 | } 175 | }; 176 | 177 | const addSelectedGames = () => { 178 | for (let game of selectedGames) { 179 | const [whitePlayer, blackPlayer] = game.split(" - "); 180 | if ( 181 | !links.some( 182 | (link) => 183 | link.whitePlayer === whitePlayer && link.blackPlayer === blackPlayer 184 | ) 185 | ) { 186 | setLinks((prevLinks) => [ 187 | ...prevLinks, 188 | { 189 | evaluation: null, 190 | whitePlayer, 191 | blackPlayer, 192 | error: null, 193 | lastFEN: "", 194 | }, 195 | ]); 196 | updateEvaluationsForLink({ whitePlayer, blackPlayer }); 197 | } 198 | } 199 | setSelectedGames([]); 200 | }; 201 | 202 | const updateEvaluationsForLink = async (link) => { 203 | const games = allGames.current.split("\n\n\n"); 204 | const specificGamePgn = games.reverse().find((game) => { 205 | const whiteNameMatch = game.match(/\[White "(.*?)"\]/); 206 | const blackNameMatch = game.match(/\[Black "(.*?)"\]/); 207 | return ( 208 | whiteNameMatch && 209 | blackNameMatch && 210 | `${whiteNameMatch[1]} - ${blackNameMatch[1]}` === 211 | `${link.whitePlayer} - ${link.blackPlayer}` 212 | ); 213 | }); 214 | 215 | if (specificGamePgn) { 216 | const cleanedPgn = specificGamePgn 217 | .split("\n") 218 | .filter((line) => !line.startsWith("[") && !line.includes("[Event")) 219 | .join(" ") 220 | .replace(/ {.*?}/g, "") 221 | .trim(); 222 | const formatName = (name) => { 223 | // Remove commas and other unwanted characters 224 | const cleanedName = name.replace(/[,.;]/g, "").trim(); 225 | const parts = cleanedName.split(" ").filter((part) => part.length > 0); // Filter empty parts 226 | 227 | // Special cases: 228 | if (parts.includes("Praggnanandhaa")) { 229 | return "Pragg"; 230 | } 231 | if (parts.includes("Praggnanandhaa,")) { 232 | return "Pragg"; 233 | } 234 | if (parts.includes("Nepomniachtchi,")) { 235 | return "Nepo"; 236 | } 237 | if (parts.includes("Nepomniachtchi")) { 238 | return "Nepo"; 239 | } 240 | if (parts.includes("Warmerdam")) { 241 | return "Max"; 242 | } 243 | if (parts.includes("Goryachkina,")) { 244 | return "Gorya"; 245 | } 246 | if (parts.includes("Goryachkina")) { 247 | return "Gorya"; 248 | } 249 | if (parts.includes("Gukesh")) { 250 | return "Gukesh"; 251 | } 252 | 253 | // Find the shortest name 254 | let shortestName = parts[0] || ""; // Initialize with empty string 255 | for (let i = 1; i < parts.length; i++) { 256 | if (parts[i].length < shortestName.length) { 257 | shortestName = parts[i]; 258 | } 259 | } 260 | 261 | return shortestName; 262 | }; 263 | 264 | let gameResult = null; 265 | const resultMatch = cleanedPgn.match(/(1-0|0-1|1\/2-1\/2)$/); 266 | if (resultMatch) { 267 | const result = resultMatch[1]; 268 | if (result === "1-0") gameResult = "1-0"; 269 | else if (result === "0-1") gameResult = "0-1"; 270 | else if (result === "1/2-1/2") gameResult = "Draw"; 271 | } 272 | 273 | const chess = new Chess(); 274 | try { 275 | chess.loadPgn(cleanedPgn); 276 | const currentFEN = chess.fen(); 277 | 278 | if (currentFEN !== link.lastFEN || gameResult !== link.result) { 279 | const evalData = await fetchEvaluation(currentFEN); 280 | setLinks((prevLinks) => { 281 | const updatedLinks = [...prevLinks]; 282 | const idx = updatedLinks.findIndex( 283 | (l) => 284 | l.whitePlayer === link.whitePlayer && 285 | l.blackPlayer === link.blackPlayer 286 | ); 287 | if (idx !== -1) { 288 | updatedLinks[idx] = { 289 | ...link, 290 | evaluation: evalData.evaluation, 291 | lastFEN: currentFEN, 292 | result: gameResult, 293 | }; 294 | } 295 | return updatedLinks; 296 | }); 297 | } 298 | } catch (error) { 299 | console.error("Error loading PGN:", error); 300 | } 301 | } 302 | }; 303 | 304 | const updateEvaluations = async () => { 305 | for (let link of links) { 306 | await updateEvaluationsForLink(link); 307 | await new Promise((resolve) => setTimeout(resolve, 200)); 308 | } 309 | }; 310 | 311 | const handleGenerateLink = () => { 312 | const stateData = { 313 | broadcastIDs, 314 | selectedGames, 315 | customStyles, 316 | }; 317 | 318 | const serializedData = encodeURIComponent(JSON.stringify(stateData)); 319 | navigator.clipboard 320 | .writeText(`${window.location.origin}/broadcast/${serializedData}`) 321 | .then(() => alert("Link copied to clipboard!")) 322 | .catch((err) => console.error("Failed to copy link:", err)); 323 | }; 324 | 325 | useEffect(() => { 326 | if (links.length) { 327 | const interval = setInterval(() => { 328 | updateEvaluations(); 329 | }, 2000); 330 | return () => clearInterval(interval); 331 | } 332 | }, [links]); 333 | 334 | useEffect(() => { 335 | const queryParams = new URLSearchParams(window.location.search); 336 | const tournamentId = queryParams.get("tournamentId"); 337 | 338 | if (tournamentId) { 339 | handleTournamentSelection([tournamentId]); 340 | } 341 | }, []); 342 | 343 | useEffect(() => { 344 | const queryParams = new URLSearchParams(window.location.search); 345 | const stateParam = queryParams.get("state"); 346 | 347 | if (stateParam) { 348 | try { 349 | const { broadcastIDs, selectedGames, customStyles } = JSON.parse( 350 | decodeURIComponent(stateParam) 351 | ); 352 | setBroadcastIDs(broadcastIDs); 353 | setSelectedGames(selectedGames); 354 | setCustomStyles(customStyles); 355 | } catch (error) { 356 | console.error("Error parsing state from URL", error); 357 | } 358 | } 359 | }, []); 360 | 361 | return ( 362 | 363 | 367 | 368 | 371 | ChessBase India Logo 376 | 377 | 378 | {isBroadcastLoaded ? ( 379 | 389 | {availableGames.map((game, index) => ( 390 | handleGameSelection(game)} 394 | isSelected={selectedGames.includes(game)} 395 | /> 396 | ))} 397 | 405 | 413 | 417 | 418 | ) : ( 419 |
420 | 421 |
422 | )} 423 |
424 | 425 | 431 | 437 | {links.map((link, index) => ( 438 | 439 | handleBlunder(index)} 449 | /> 450 | 451 | ))} 452 | 453 | 454 |
456 | ); 457 | } 458 | 459 | export default App; 460 | -------------------------------------------------------------------------------- /src/app/evalbars/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | export { default as LandingPage } from "./landing-page/LandingPage"; 2 | export { default as App } from "./evalbars/App"; 3 | export { default as Ccm } from "./ccm/ccm"; 4 | export { default as Messagedisplay } from "./message-display/messagedisplay"; 5 | -------------------------------------------------------------------------------- /src/app/landing-page/LandingPage.css: -------------------------------------------------------------------------------- 1 | .landing-container { 2 | color: #fff; 3 | background-color: #180f26; 4 | font-family: 'Arial', sans-serif; 5 | min-height: 100vh; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | header { 11 | padding: 20px; 12 | text-align: center; 13 | } 14 | 15 | header img { 16 | max-width: 30%; 17 | height: auto; 18 | } 19 | 20 | main { 21 | flex-grow: 1; 22 | padding: 20px; 23 | } 24 | 25 | .intro h1 { 26 | text-align: center; 27 | margin-bottom: 10px; 28 | font-size: 2.5rem; 29 | } 30 | 31 | .intro p { 32 | text-align: center; 33 | margin-bottom: 20px; 34 | } 35 | 36 | .products { 37 | display: grid; 38 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 39 | gap: 20px; 40 | } 41 | 42 | .product { 43 | background-color: #4f4ea092; 44 | padding: 20px; 45 | border-radius: 8px; 46 | text-decoration: none; 47 | color: inherit; 48 | } 49 | .product:hover { 50 | transform : scale(1.1); 51 | border : 5px solid #ffffff; 52 | transition: all 0.2s ease-in-out; 53 | } 54 | 55 | .product img { 56 | width: 100%; 57 | border-radius: 4px; 58 | margin-bottom: 10px; 59 | } 60 | 61 | 62 | footer { 63 | background-color: #333; 64 | padding: 10px; 65 | text-align: center; 66 | color: #fff 67 | } 68 | 69 | footer a { 70 | color: #FF0000; /* Your desired color */ 71 | } 72 | 73 | @media (max-width: 768px) { 74 | .products { 75 | grid-template-columns: 1fr; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/landing-page/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './LandingPage.css'; 3 | 4 | function LandingPage() { 5 | return ( 6 |
7 |
8 | Chessbase India Logo 9 |
10 |
11 |
12 |

Welcome to Broadcast Manager for Chessbase India

13 |

Enhance your chess broadcasts with cutting-edge features.

14 |
15 | 16 |
17 | 18 | Evaluation Bars 19 |

Evaluation Bars

20 |

Visualize game dynamics with multiple evaluation bars.

21 |
22 | 23 | Chat Chess Moves 24 |

Chat Chess Moves

25 |

Engage your audience with interactive chess puzzles in the chat.

26 |
27 | 28 | Display Messages in Broadcast 29 |

Display Messages in Broadcast

30 |

Feature live chat messages directly in your stream.

31 |
32 |
33 |
34 | 35 |
36 |

© Chessbase India - All rights reserved

37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | export default LandingPage; 44 | -------------------------------------------------------------------------------- /src/app/message-display/messagedisplay.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function messagedisplay(){ 4 | return( 5 |
COMING SOON!
6 | ) 7 | } 8 | 9 | export default messagedisplay; -------------------------------------------------------------------------------- /src/assets/blunder-sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chess-broadcasting-tools/eval-bar/d9d2d69a87a2074680adea50a43a8ce2df9cca00/src/assets/blunder-sound.mp3 -------------------------------------------------------------------------------- /src/components/customize-evalbar/CustomizeEvalBar.css: -------------------------------------------------------------------------------- 1 | /* Base Styling for the Customize Evaluation Bar Container */ 2 | .customize-eval-bar { 3 | background-color: #003366; /* Deep navy blue background */ 4 | padding: 20px; 5 | border-radius: 12px; 6 | box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); 7 | color: #ffffff; 8 | transition: all 0.3s ease-in-out; 9 | } 10 | 11 | /* Hover effect for the entire container */ 12 | .customize-eval-bar:hover { 13 | transform: translateY(-5px); 14 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4); 15 | } 16 | 17 | /* Styling for buttons and labels */ 18 | .customize-eval-bar button, .customize-eval-bar label { 19 | background-color: #0077B6; /* Vibrant blue */ 20 | color: white; 21 | border: none; 22 | padding: 12px 24px; 23 | border-radius: 8px; 24 | cursor: pointer; 25 | text-align: center; 26 | font-size: 18px; 27 | font-weight: bold; 28 | transition: all 0.3s ease-in-out; 29 | } 30 | 31 | /* Hover effect for buttons and labels */ 32 | .customize-eval-bar button:hover, .customize-eval-bar label:hover { 33 | background-color: #0096c7; /* Lighter blue */ 34 | box-shadow: 0 5px 15px rgba(0,0,0,0.2); 35 | } 36 | 37 | /* Customization content area */ 38 | .customization-content { 39 | margin-top: 20px; 40 | border: 2px solid #0096c7; 41 | padding: 20px; 42 | border-radius: 12px; 43 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 44 | background-color: #001d3d; /* Darker shade for contrast */ 45 | transition: all 0.3s ease-in-out; 46 | } 47 | 48 | /* Dropdown Select Component */ 49 | .customization-content select { 50 | width: 100%; 51 | padding: 10px; 52 | margin-bottom: 15px; 53 | border-radius: 8px; 54 | border: 2px solid #0077B6; 55 | background-color: #0d0b0b; 56 | color: #e0e3e7; 57 | font-size: 16px; 58 | transition: border-color 0.3s ease; 59 | } 60 | 61 | .customization-content select:focus { 62 | border-color: #25bdef; 63 | } 64 | 65 | /* Save Button Styling */ 66 | .save-button { 67 | background-color: #28a745; /* Green for confirmation */ 68 | color: white; 69 | border: none; 70 | padding: 12px 24px; 71 | border-radius: 8px; 72 | cursor: pointer; 73 | transition: background-color 0.3s ease-in-out; 74 | } 75 | 76 | .save-button:hover { 77 | background-color: #218838; /* Darker green on hover */ 78 | box-shadow: 0 5px 15px rgba(0,0,0,0.2); 79 | } 80 | 81 | /* Color Picker Container */ 82 | .color-picker-container { 83 | border: 2px solid #0077B6; 84 | padding: 15px; 85 | border-radius: 12px; 86 | background-color: #010000; 87 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 88 | transition: all 0.3s ease-in-out; 89 | } 90 | 91 | /* Label for Currently Customizing Component */ 92 | .customizing-label { 93 | margin-bottom: 15px; 94 | font-size: 1.4rem; 95 | color: #d1e3f4; 96 | font-weight: bold; 97 | transition: color 0.3s ease-in-out; 98 | } 99 | 100 | /* General button styling */ 101 | .primary-button { 102 | width: auto; /* Adjust width as needed */ 103 | padding: 10px 20px; 104 | margin: 5px; /* Spacing between buttons */ 105 | display: inline-block; /* For layout */ 106 | transition: transform 0.2s ease-in-out; 107 | } 108 | 109 | .primary-button:hover { 110 | transform: scale(1.05); 111 | } 112 | -------------------------------------------------------------------------------- /src/components/customize-evalbar/CustomizeEvalBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { MuiColorInput } from 'mui-color-input'; 3 | import './CustomizeEvalBar.css'; // Ensure this CSS file is correctly linked 4 | 5 | function CustomizeEvalBar({ customStyles, setCustomStyles }) { 6 | const [selectedComponent, setSelectedComponent] = useState(''); 7 | const [showCustomization, setShowCustomization] = useState(false); 8 | 9 | const componentsList = [ 10 | { value: 'evalContainerBg', label: 'Eval Container Background' }, 11 | { value: 'blackBarColor', label: 'Black Bar Color' }, 12 | { value: 'whiteBarColor', label: 'White Bar Color' }, 13 | { value: 'whitePlayerColor', label: 'White Player Color' }, 14 | { value: 'blackPlayerColor', label: 'Black Player Color' }, 15 | { value: 'whitePlayerNameColor', label: 'White Player Name Color' }, 16 | { value: 'blackPlayerNameColor', label: 'Black Player Name Color' }, 17 | { value: 'evalContainerBorderColor', label: 'Eval Container Border Color' } 18 | ]; 19 | 20 | const handleChangeComplete = (color) => { 21 | if (selectedComponent) { 22 | setCustomStyles({ ...customStyles, [selectedComponent]: color }); 23 | } 24 | }; 25 | 26 | const handleSaveStyles = () => { 27 | const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(customStyles)); 28 | const downloadAnchorNode = document.createElement('a'); 29 | downloadAnchorNode.setAttribute("href", dataStr); 30 | downloadAnchorNode.setAttribute("download", "colorProfile.json"); 31 | document.body.appendChild(downloadAnchorNode); 32 | downloadAnchorNode.click(); 33 | downloadAnchorNode.remove(); 34 | }; 35 | 36 | const handleImportStyles = (event) => { 37 | const fileReader = new FileReader(); 38 | fileReader.readAsText(event.target.files[0], "UTF-8"); 39 | fileReader.onload = e => { 40 | const importedStyles = JSON.parse(e.target.result); 41 | setCustomStyles(importedStyles); 42 | }; 43 | }; 44 | 45 | return ( 46 |
47 | 50 | 51 | 52 | 53 | 54 | {showCustomization && ( 55 |
56 | 62 | 63 | 64 | 65 | {selectedComponent && ( 66 |
67 |

Customizing: {selectedComponent.split(/(?=[A-Z])/).join(" ")}

68 | handleChangeComplete(color)} 71 | /> 72 |
73 | )} 74 |
75 | )} 76 |
77 | ); 78 | } 79 | 80 | export default CustomizeEvalBar; 81 | -------------------------------------------------------------------------------- /src/components/evalbar/EvalBar.css: -------------------------------------------------------------------------------- 1 | .eval-container { 2 | flex-wrap: wrap; /* Allow containers to wrap to the next line */ 3 | padding: 3px; /* Adjusted padding for a more compact look */ 4 | width: 90%; /* Each container occupies 25% of the parent container */ 5 | margin: 5px; /* Adjusted for horizontal and vertical spacing */ 6 | padding: 5px; 7 | background: linear-gradient(90deg, rgb(65, 63, 63) 50%, rgb(55, 53, 53) 89%); 8 | border-radius: 1px; /* Sharper borders */ 9 | box-shadow: 0 4px 10px rgba(10, 9, 9, 0); 10 | position: relative;/* Include padding and border in width calculation */ 11 | height:auto; 12 | } 13 | 14 | 15 | .evaluation-value { 16 | font-weight: bold; 17 | color: rgb(0, 0, 0); 18 | font-size: 12px; /* Adjusted font size */ 19 | margin-top: 9px; /* Moved above the player names */ 20 | margin-bottom: 5px; 21 | position: absolute; /* Position absolutely */ 22 | top: 53%; /* Align to the top */ 23 | left: 50%; /* Center horizontally */ 24 | transform: translateX(-50%); /* Center horizontally */ 25 | z-index: 1; /* Ensure it's above bars */ 26 | } 27 | 28 | .player-names { 29 | display: flex; 30 | justify-content: space-between; 31 | width: 100%; 32 | margin-bottom: 4px; /* Reduced margin for compactness */ 33 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 34 | } 35 | 36 | .white-player, .black-player { 37 | flex: 1; 38 | padding: 2px 8px; /* Adjusted padding for sleeker design */ 39 | border-radius: 3px; /* Sharper borders */ 40 | font-size: 12px; /* Smaller font size for sleeker design */ 41 | } 42 | 43 | .white-player { 44 | background-color: #ecdab900; 45 | color: black; 46 | text-align: left; 47 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 48 | 49 | } 50 | 51 | .black-player { 52 | background-color: #ae8a6900; 53 | color: rgb(0, 0, 0); 54 | text-align: right; 55 | font-family: 'Times New Roman', Times, serif; 56 | } 57 | 58 | .eval-bars { 59 | position: relative; 60 | display: flex; 61 | width: 100%; /* Change this to the desired width for the bars */ 62 | height: 11px; 63 | background-color: #000000; 64 | border-radius: 20px; 65 | overflow: hidden; 66 | margin-top: 5px; 67 | margin-bottom: 5px; 68 | } 69 | 70 | .white-bar { 71 | position: absolute; 72 | left: 0; 73 | height: 100%; 74 | background-color: #ffffff; 75 | transition: width 1.2s; 76 | } 77 | 78 | .result-value { 79 | font-weight: bold; 80 | color: rgb(255, 255, 255); /* Set the text color to white for result */ 81 | font-size: 18px; 82 | top: 100%; /* Align to the top */ 83 | left: 15%; 84 | background-size: 300px; 85 | } 86 | 87 | .evaluation-value { 88 | font-weight: bold; 89 | color: black; /* Set the text color to black for evaluation */ 90 | font-size: 18px; 91 | } 92 | 93 | .eval-container.blink-border { 94 | animation: blink-border 1s infinite; 95 | } 96 | 97 | @keyframes blink-border { 98 | 0%, 100% { 99 | border-color: red; 100 | border-width: 6px; 101 | } 102 | 50% { 103 | border-color: white; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/evalbar/Evalbar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import React, { useEffect, useRef } from "react"; 3 | import { Box, Typography } from "@mui/material"; 4 | import "./EvalBar.css"; 5 | import blunderSound from "../../assets/blunder-sound.mp3"; 6 | 7 | function EvalBar({ 8 | evaluation, 9 | whitePlayer, 10 | blackPlayer, 11 | result, 12 | layout, 13 | customStyles, 14 | alert, 15 | onBlunder, 16 | lastFEN, 17 | }) { 18 | const prevEvaluationRef = useRef(null); 19 | const prevResultRef = useRef(undefined); 20 | const blunderSoundRef = useRef(null); 21 | 22 | const [displayBlunder, setDisplayBlunder] = React.useState(false); 23 | 24 | const onBlunderFunction = () => { 25 | onBlunder(); 26 | setDisplayBlunder(true); 27 | setTimeout(() => { 28 | setDisplayBlunder(false); 29 | }, 60000); 30 | }; 31 | 32 | useEffect(() => { 33 | if (prevEvaluationRef.current !== null) { 34 | const prevEval = prevEvaluationRef.current; 35 | const currentEval = evaluation; 36 | 37 | const isBlunder = (prevEval, currentEval) => { 38 | if (prevEval >= -4 && prevEval <= 4) { 39 | if (Math.abs(currentEval - prevEval) >= 0.6) { 40 | return true; 41 | } 42 | } 43 | return false; 44 | }; 45 | 46 | if (isBlunder(prevEval, currentEval)) { 47 | onBlunderFunction(); 48 | } 49 | } 50 | prevEvaluationRef.current = evaluation; 51 | }, [evaluation, onBlunder]); 52 | 53 | useEffect(() => { 54 | if ( 55 | prevResultRef.current !== undefined && 56 | prevResultRef.current !== result && 57 | result !== null 58 | ) { 59 | blunderSoundRef.current.volume = 0.8; // Set volume to 60% 60 | blunderSoundRef.current.play(); 61 | onBlunder(); 62 | } 63 | prevResultRef.current = result; 64 | }, [result, onBlunder]); 65 | 66 | const getBarSegment = (evalValue) => { 67 | return Math.min(Math.max(Math.round(evalValue), -5), +5); 68 | }; 69 | 70 | const getWhiteBarWidth = () => { 71 | if (evaluation >= 99) return "100%"; 72 | if (evaluation >= 4) return "90%"; 73 | if (evaluation <= -4) return "10%"; 74 | return `${50 + getBarSegment(evaluation) * 7.5}%`; 75 | }; 76 | 77 | const formatName = (name) => { 78 | // Remove commas and other unwanted characters 79 | const cleanedName = name.replace(/[,.;]/g, "").trim(); 80 | const parts = cleanedName.split(" ").filter((part) => part.length > 0); // Filter empty parts 81 | 82 | // Special cases: 83 | if (parts.includes("Praggnanandhaa")) { 84 | return "Pragg"; 85 | } 86 | if (parts.includes("Nepomniachtchi")) { 87 | return "Nepo"; 88 | } 89 | if (parts.includes("Goryachkina")) { 90 | return "Gorya"; 91 | } 92 | if (parts.includes("Gukesh")) { 93 | return "Gukesh"; 94 | } 95 | 96 | // Find the shortest name 97 | let shortestName = parts[0] || ""; // Initialize with empty string 98 | for (let i = 1; i < parts.length; i++) { 99 | if (parts[i].length < shortestName.length) { 100 | shortestName = parts[i]; 101 | } 102 | } 103 | 104 | return shortestName; 105 | }; 106 | 107 | const formatEvaluation = (evalValue) => { 108 | if (evalValue < -1000 || evalValue > 1000) { 109 | return "Checkmate"; 110 | } 111 | return evalValue; 112 | }; 113 | 114 | const displayResult = 115 | result !== null ? formatEvaluation(result) : formatEvaluation(evaluation); 116 | const evalDisplayClass = result !== null ? "result" : "evaluation-value"; 117 | 118 | return ( 119 | 128 | 133 | 143 | {formatName(whitePlayer)} 144 | 145 | 155 | {formatName(blackPlayer)} 156 | 157 | 158 |
169 | {lastFEN && " #" + getLastMove(lastFEN)} 170 |
171 | 172 | 190 | {displayResult} 191 | 192 | 193 | {!result && ( 194 | 206 | 213 | 214 | 215 | )} 216 |
236 | ); 237 | } 238 | 239 | // get the last move from FEN 240 | 241 | function getLastMove(fen) { 242 | const parts = fen.split(" "); 243 | if (parts.length < 4) return null; 244 | return parts[parts.length - 1]; 245 | } 246 | 247 | export default EvalBar; 248 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as EvalBar } from "./evalbar/Evalbar"; 2 | export { default as CustomizeEvalBar } from "./customize-evalbar/CustomizeEvalBar"; 3 | export { default as TournamentsList } from "./tournaments-list/TournamentsList"; 4 | -------------------------------------------------------------------------------- /src/components/tournaments-list/TournamentsList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const TournamentsWrapper = styled.div` 5 | margin-top: 5rem; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | `; 10 | const NoBroadcastsMessage = styled.p` 11 | color: #faf9f6; /* White color */ 12 | font-size: 1.2em; /* Bigger font size */ 13 | `; 14 | 15 | const Card = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | align-items: flex-start; 19 | border: ${(props) => 20 | props.selected ? "10px solid #4CAF50" : "1px solid #ccc"}; 21 | padding: 1rem; 22 | margin: 1rem 0; 23 | border-radius: 1rem; 24 | cursor: pointer; 25 | background-color: ${(props) => 26 | props.selected ? "rgba(76, 175, 80, 0.3)" : "rgba(1, 1, 4, 0.6)"}; 27 | transition: all 0.3s ease-in-out; 28 | transform: perspective(1px) translateZ(0); 29 | width: 80%; 30 | max-width: 600px; 31 | 32 | &:hover { 33 | box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2); 34 | border-color: #4caf50; 35 | transform: scale(1.05); 36 | } 37 | 38 | .card-image { 39 | width: 100%; /* Adjust the width of the image */ 40 | height: auto; /* Maintain aspect ratio */ 41 | margin-bottom: 1rem; /* Add some space below the image */ 42 | } 43 | `; 44 | 45 | const CardHeader = styled.div` 46 | display: flex; 47 | justify-content: space-between; 48 | width: 100%; 49 | `; 50 | 51 | const CardTitle = styled.h2` 52 | font-size: 1.8em; 53 | color: #faf9f6; 54 | margin-bottom: 1rem; 55 | `; 56 | 57 | const CardDate = styled.p` 58 | font-size: 1em; 59 | color: #faf9f6; 60 | margin-bottom: 1rem; 61 | `; 62 | 63 | const CardDescription = styled.p` 64 | font-size: 1em; 65 | color: #faf9f6; 66 | margin-bottom: 1rem; 67 | height: 4em; 68 | overflow: hidden; 69 | text-overflow: ellipsis; 70 | `; 71 | 72 | const Button = styled.a` 73 | margin-top: 0.5rem; 74 | padding: 0.5rem 1rem; 75 | background-color: #4caf50; 76 | color: white; 77 | border-radius: 0.5rem; 78 | cursor: pointer; 79 | transition: background-color 0.3s, box-shadow 0.3s; 80 | 81 | &:hover { 82 | background-color: #36a420; 83 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 84 | } 85 | `; 86 | 87 | const Title = styled.h1` 88 | border-bottom: 5px solid #4caf50; 89 | padding-bottom: 1rem; 90 | font-size: 2em; 91 | font-weight: bold; 92 | color: #4caf50; 93 | text-align: center; 94 | margin-bottom: 3rem; 95 | `; 96 | 97 | const SearchWrapper = styled.div` 98 | display: flex; 99 | justify-content: center; 100 | margin-bottom: 2rem; 101 | `; 102 | 103 | const SearchInput = styled.input` 104 | margin-right: 1rem; 105 | padding: 0.5rem; 106 | font-size: 1em; 107 | `; 108 | 109 | const SearchButton = styled.button` 110 | padding: 0.5rem 1rem; 111 | background-color: #4caf50; 112 | color: white; 113 | border: none; 114 | border-radius: 0.5rem; 115 | cursor: pointer; 116 | transition: background-color 0.3s; 117 | 118 | &:hover { 119 | background-color: #36a420; 120 | } 121 | `; 122 | 123 | const TournamentsList = ({ onSelect }) => { 124 | const [tournaments, setTournaments] = useState([]); 125 | const [filteredTournaments, setFilteredTournaments] = useState([]); 126 | const [searchTerm, setSearchTerm] = useState(""); 127 | const [selectedTournaments, setSelectedTournaments] = useState([]); 128 | const [checkedItems, setCheckedItems] = useState({}); 129 | const [customUrl, setCustomUrl] = useState(""); 130 | const [tournamentId, setTournamentId] = useState(""); 131 | const [broadcasts, setBroadcasts] = useState(true); 132 | 133 | useEffect(() => { 134 | fetch("https://lichess.org/api/broadcast?nb=50") 135 | .then((response) => response.text()) 136 | .then((data) => { 137 | const jsonData = data 138 | .trim() 139 | .split("\n") 140 | .map((line) => JSON.parse(line)); 141 | const ongoingTournaments = jsonData.filter( 142 | (tournament) => 143 | tournament.rounds && 144 | tournament.rounds.some((round) => round.ongoing === true) 145 | ); 146 | setTournaments(ongoingTournaments); 147 | setFilteredTournaments(ongoingTournaments); 148 | if (ongoingTournaments.length === 0) { 149 | setBroadcasts(false); 150 | } 151 | }) 152 | .catch((error) => 153 | console.error("Error fetching tournaments:", error) 154 | ); 155 | }, []); 156 | 157 | const handleSearch = () => { 158 | const lowerCaseSearchTerm = searchTerm.toLowerCase(); 159 | const filtered = tournaments.filter((tournament) => 160 | tournament.tour.name.toLowerCase().includes(lowerCaseSearchTerm) 161 | ); 162 | setFilteredTournaments(filtered); 163 | }; 164 | 165 | const handleCustomUrlChange = (e) => { 166 | setCustomUrl(e.target.value); 167 | const urlParts = e.target.value.split("/"); 168 | const id = urlParts[urlParts.length - 1]; 169 | setTournamentId(id); 170 | }; 171 | 172 | const onSelectTournament = () => { 173 | if (tournamentId) { 174 | setSelectedTournaments([tournamentId]); 175 | onSelect([tournamentId]); // This line is added to simulate the selection 176 | } 177 | }; 178 | 179 | return ( 180 | 181 | LIVE BROADCASTS 182 | 183 | setSearchTerm(e.target.value)} 186 | placeholder="Search tournaments..." 187 | /> 188 | Search 189 | 190 | 195 | Go 196 | 197 | 205 | {filteredTournaments.map((tournament, index) => 206 | tournament.tour && tournament.rounds && tournament.rounds.length > 0 ? ( 207 | 211 | {tournament.image && ( 212 | Tournament Image 217 | )} 218 | { 222 | setCheckedItems((prevState) => ({ 223 | ...prevState, 224 | [tournament.tour.id]: !prevState[tournament.tour.id], 225 | })); 226 | const ongoingRound = tournament.rounds.find( 227 | (round) => round.ongoing === true 228 | ); 229 | if (ongoingRound) { 230 | setSelectedTournaments((prevTournaments) => 231 | prevTournaments.includes(ongoingRound.id) 232 | ? prevTournaments.filter((id) => id !== ongoingRound.id) 233 | : [...prevTournaments, ongoingRound.id] 234 | ); 235 | } 236 | }} 237 | /> 238 | 239 | {tournament.tour.name} 240 | {tournament.tour.date} 241 | 242 | {tournament.tour.description} 243 | 250 | 251 | ) : null 252 | )} 253 | 254 | ); 255 | }; 256 | 257 | export default TournamentsList; 258 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chess-broadcasting-tools/eval-bar/d9d2d69a87a2074680adea50a43a8ce2df9cca00/src/index.css -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 4 | import { LandingPage, App, Ccm } from "./app"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | } /> 11 | } /> 12 | } /> 13 | 14 | 15 | , 16 | document.getElementById("root") 17 | ); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | gunicorn -w 4 chess_api:app -b 0.0.0.0:5000 3 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | 1) Add routing 2 | 2) Improve the visuals of blunder alert 3 | 3) Refactor the code and improve the code quality 4 | i) Use better variable names 5 | ii) Have styles, utils, folders 6 | --------------------------------------------------------------------------------