├── README.md ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── og-image.png ├── robots.txt ├── manifest.json └── index.html ├── src ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── logo.svg ├── App.css └── App.js ├── .gitignore └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # SUPERVISION 2 | 3 | A time-based color matching game 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmrs/colormatch/main/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmrs/colormatch/main/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmrs/colormatch/main/public/logo512.png -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmrs/colormatch/main/public/og-image.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-match", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 7 | "@emotion/react": "^11.11.3", 8 | "@testing-library/jest-dom": "^5.17.0", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "chroma-js": "^2.4.2", 12 | "feather-icons-react": "^0.7.0", 13 | "react": "^18.2.0", 14 | "react-color": "^2.19.3", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1", 17 | "react-toastify": "^10.0.4", 18 | "uuid": "^9.0.1", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | 30 | Supervision 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | transition: background-color .15s ease, color .15s ease, border-color .15s ease; 10 | user-select: none; 11 | } 12 | 13 | 14 | 15 | button { 16 | width: 140px; 17 | appearance: none; 18 | -webkit-appearance: none; 19 | } 20 | 21 | 22 | .animated-button { 23 | border: 0; 24 | background: linear-gradient(-30deg, #0b1b3d 50%, #08142b 50%); 25 | padding: 12px 20px; 26 | margin-top: 12px; 27 | display: inline-block; 28 | -webkit-transform: translate(0%, 0%); 29 | transform: translate(0%, 0%); 30 | overflow: hidden; 31 | color: white; 32 | font-size: 12px; 33 | font-weight: 700; 34 | text-align: center; 35 | text-decoration: none; 36 | -webkit-box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); 37 | box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); 38 | } 39 | 40 | .animated-button::before { 41 | content: 'Submit Match'; 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | position: absolute; 46 | top: 0px; 47 | left: 0px; 48 | width: 100%; 49 | height: 100%; 50 | background-color: #8592ad; 51 | opacity: 0; 52 | -webkit-transition: .2s opacity ease-in-out; 53 | transition: .2s opacity ease-in-out; 54 | background: linear-gradient(-30deg, blue 0%, navy 100%); 55 | } 56 | 57 | .animated-button:hover::before { 58 | opacity: 1; 59 | } 60 | 61 | .animated-button span { 62 | position: absolute; 63 | } 64 | 65 | .animated-button span:nth-child(1) { 66 | top: 0px; 67 | left: 0px; 68 | width: 100%; 69 | height: 4px; 70 | background: -webkit-gradient(linear, right top, left top, from(rgba(8, 20, 43, 0)), to(#ff33cc)); 71 | background: linear-gradient(to left, rgba(8, 20, 43, 0), #ff33cc); 72 | -webkit-animation: 2s animateTop linear infinite; 73 | animation: 2s animateTop linear infinite; 74 | } 75 | 76 | @-webkit-keyframes animateTop { 77 | 0% { 78 | -webkit-transform: translateX(100%); 79 | transform: translateX(100%); 80 | } 81 | 100% { 82 | -webkit-transform: translateX(-100%); 83 | transform: translateX(-100%); 84 | } 85 | } 86 | 87 | @keyframes animateTop { 88 | 0% { 89 | -webkit-transform: translateX(100%); 90 | transform: translateX(100%); 91 | } 92 | 100% { 93 | -webkit-transform: translateX(-100%); 94 | transform: translateX(-100%); 95 | } 96 | } 97 | 98 | .animated-button span:nth-child(2) { 99 | top: 0px; 100 | right: 0px; 101 | height: 100%; 102 | width: 4px; 103 | background: -webkit-gradient(linear, left bottom, left top, from(rgba(8, 20, 43, 0)), to(#ff33cc)); 104 | background: linear-gradient(to top, rgba(8, 20, 43, 0), #ff33cc); 105 | -webkit-animation: 2s animateRight linear -1s infinite; 106 | animation: 2s animateRight linear -1s infinite; 107 | } 108 | 109 | @-webkit-keyframes animateRight { 110 | 0% { 111 | -webkit-transform: translateY(100%); 112 | transform: translateY(100%); 113 | } 114 | 100% { 115 | -webkit-transform: translateY(-100%); 116 | transform: translateY(-100%); 117 | } 118 | } 119 | 120 | @keyframes animateRight { 121 | 0% { 122 | -webkit-transform: translateY(100%); 123 | transform: translateY(100%); 124 | } 125 | 100% { 126 | -webkit-transform: translateY(-100%); 127 | transform: translateY(-100%); 128 | } 129 | } 130 | 131 | .animated-button span:nth-child(3) { 132 | bottom: 0px; 133 | left: 0px; 134 | width: 100%; 135 | height: 4px; 136 | background: -webkit-gradient(linear, left top, right top, from(rgba(8, 20, 43, 0)), to(#ff33cc)); 137 | background: linear-gradient(to right, rgba(8, 20, 43, 0), #ff33cc); 138 | -webkit-animation: 2s animateBottom linear infinite; 139 | animation: 2s animateBottom linear infinite; 140 | } 141 | 142 | @-webkit-keyframes animateBottom { 143 | 0% { 144 | -webkit-transform: translateX(-100%); 145 | transform: translateX(-100%); 146 | } 147 | 100% { 148 | -webkit-transform: translateX(100%); 149 | transform: translateX(100%); 150 | } 151 | } 152 | 153 | @keyframes animateBottom { 154 | 0% { 155 | -webkit-transform: translateX(-100%); 156 | transform: translateX(-100%); 157 | } 158 | 100% { 159 | -webkit-transform: translateX(100%); 160 | transform: translateX(100%); 161 | } 162 | } 163 | 164 | .animated-button span:nth-child(4) { 165 | top: 0px; 166 | left: 0px; 167 | height: 100%; 168 | width: 4px; 169 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(8, 20, 43, 0)), to(#ff33cc)); 170 | background: linear-gradient(to bottom, rgba(8, 20, 43, 0), #ff33cc); 171 | -webkit-animation: 2s animateLeft linear -1s infinite; 172 | animation: 2s animateLeft linear -1s infinite; 173 | } 174 | 175 | @-webkit-keyframes animateLeft { 176 | 0% { 177 | -webkit-transform: translateY(-100%); 178 | transform: translateY(-100%); 179 | } 180 | 100% { 181 | -webkit-transform: translateY(100%); 182 | transform: translateY(100%); 183 | } 184 | } 185 | 186 | @keyframes animateLeft { 187 | 0% { 188 | -webkit-transform: translateY(-100%); 189 | transform: translateY(-100%); 190 | } 191 | 100% { 192 | -webkit-transform: translateY(100%); 193 | transform: translateY(100%); 194 | } 195 | } 196 | 197 | .animated-button-1::before { 198 | content: 'Play Again'!important; 199 | } 200 | 201 | dl { font-family: monospace, monospace; } 202 | dt { font-family: sans-serif; font-size: 14px; } 203 | dd { margin: 0; } 204 | 205 | 206 | /* 207 | 208 | DISPLAY 209 | Docs: http://tachyons.io/docs/layout/display 210 | 211 | Base: 212 | d = display 213 | 214 | Modifiers: 215 | n = none 216 | b = block 217 | ib = inline-block 218 | it = inline-table 219 | t = table 220 | tc = table-cell 221 | t-row = table-row 222 | t-columm = table-column 223 | t-column-group = table-column-group 224 | 225 | Media Query Extensions: 226 | -ns = not-small 227 | -m = medium 228 | -l = large 229 | 230 | */ 231 | 232 | .dn { display: none; } 233 | .di { display: inline; } 234 | .db { display: block; } 235 | .dib { display: inline-block; } 236 | .dit { display: inline-table; } 237 | .dt { display: table; } 238 | .dtc { display: table-cell; } 239 | .dt-row { display: table-row; } 240 | .dt-row-group { display: table-row-group; } 241 | .dt-column { display: table-column; } 242 | .dt-column-group { display: table-column-group; } 243 | 244 | /* 245 | This will set table to full width and then 246 | all cells will be equal width 247 | */ 248 | .dt--fixed { 249 | table-layout: fixed; 250 | width: 100%; 251 | } 252 | .flex { 253 | display: flex; 254 | } 255 | 256 | @media screen and (min-width: 30em) { 257 | .dn-ns { display: none; } 258 | .di-ns { display: inline; } 259 | .db-ns { display: block; } 260 | .dib-ns { display: inline-block; } 261 | .dit-ns { display: inline-table; } 262 | .dt-ns { display: table; } 263 | .dtc-ns { display: table-cell; } 264 | .dt-row-ns { display: table-row; } 265 | .dt-row-group-ns { display: table-row-group; } 266 | .dt-column-ns { display: table-column; } 267 | .dt-column-group-ns { display: table-column-group; } 268 | 269 | .dt--fixed-ns { 270 | table-layout: fixed; 271 | width: 100%; 272 | } 273 | .flex-ns { 274 | display: flex; 275 | } 276 | } 277 | 278 | @media screen and (min-width: 48em) { 279 | .dn-m { display: none; } 280 | .di-m { display: inline; } 281 | .db-m { display: block; } 282 | .dib-m { display: inline-block; } 283 | .dit-m { display: inline-table; } 284 | .dt-m { display: table; } 285 | .dtc-m { display: table-cell; } 286 | .dt-row-m { display: table-row; } 287 | .dt-row-group-m { display: table-row-group; } 288 | .dt-column-m { display: table-column; } 289 | .dt-column-group-m { display: table-column-group; } 290 | 291 | .dt--fixed-m { 292 | table-layout: fixed; 293 | width: 100%; 294 | } 295 | .flex-m { 296 | display: flex; 297 | } 298 | } 299 | 300 | @media screen and (min-width: 64em) { 301 | .dn-l { display: none; } 302 | .di-l { display: inline; } 303 | .db-l { display: block; } 304 | .dib-l { display: inline-block; } 305 | .dit-l { display: inline-table; } 306 | .dt-l { display: table; } 307 | .dtc-l { display: table-cell; } 308 | .dt-row-l { display: table-row; } 309 | .dt-row-group-l { display: table-row-group; } 310 | .dt-column-l { display: table-column; } 311 | .dt-column-group-l { display: table-column-group; } 312 | 313 | .dt--fixed-l { 314 | table-layout: fixed; 315 | width: 100%; 316 | } 317 | .flex-l { 318 | display: flex; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import chroma from 'chroma-js'; 3 | import { RefreshCcw } from 'feather-icons-react'; 4 | import { v4 as uuidv4 } from 'uuid' 5 | import { ToastContainer, toast } from 'react-toastify'; 6 | import 'react-toastify/dist/ReactToastify.css'; 7 | import { ChromePicker } from 'react-color'; 8 | import './App.css' 9 | 10 | const Badge = ({ color = 'black', backgroundColor, ...props }) => { 11 | return ( 12 | 13 | {props.children} 14 | 15 | ) 16 | } 17 | 18 | 19 | const chromePickerStyles = { 20 | default: { 21 | hue: { // See the individual picker source for which keys to use 22 | height: '16px', 23 | }, 24 | }, 25 | } 26 | 27 | 28 | const ScoreModal = ({ isOpen, onClose, scores, title, currentUser }) => { 29 | if (!isOpen) return null; 30 | 31 | return ( 32 |
33 | 34 |

{title}

35 |
    36 | {scores.map((score, index) => ( 37 |
  1. {score.user} {score.score.toFixed(3)}
  2. 40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | 46 | function App() { 47 | // const [color, setColor] = useState('#ff0000'); 48 | const [randomColor, setRandomColor] = useState(null); 49 | const [selectedColor, setSelectedColor] = useState('#ffffff'); 50 | const [startTime, setStartTime] = useState(0); 51 | const [score, setScore] = useState(null); 52 | const [timeTaken, setTimeTaken] = useState(0); 53 | const [accuracy, setAccuracy] = useState(0); 54 | const [elapsedTime, setElapsedTime] = useState(0); 55 | const [timerId, setTimerId] = useState(null); 56 | const [dailyScores, setDailyScores] = useState([]); 57 | const [allTimeScores, setAllTimeScores] = useState([]); 58 | const [showDailyTop100Modal, setShowDailyTop100Modal] = useState(false); 59 | const [showAllTimeTop100Modal, setShowAllTimeTop100Modal] = useState(false); 60 | 61 | const [pixelPerfectBadge, setPixelPerfectBadge] = useState(false); 62 | 63 | const [username, setUsername] = useState(() => localStorage.getItem('username') || ''); 64 | const [highScore, setHighScore] = useState(() => { 65 | // Retrieve the high score from local storage or set it to 0 66 | return localStorage.getItem('highScore') || 0; 67 | }); 68 | const [gameHistory, setGameHistory] = useState(() => { 69 | const saved = localStorage.getItem('gameHistory'); 70 | return saved ? JSON.parse(saved) : []; 71 | }); 72 | const [showPlayAgain, setShowPlayAgain] = useState(false); 73 | 74 | const [count, setCount] = useState(0); 75 | 76 | const durableObjectName = 'COUNTER_COLORMATCH'; // Replace with the actual name of your Durable Object 77 | const fetchCount = async () => { 78 | try { 79 | const response = await fetch(`https://ts-gen-count.adam-f8f.workers.dev/?name=${durableObjectName}`); 80 | const data = await response.text(); 81 | setCount(data); 82 | } catch (error) { 83 | console.error('Error fetching count:', error); 84 | } 85 | }; 86 | 87 | const handleIncrement = async () => { 88 | try { 89 | await fetch(`https://ts-gen-count.adam-f8f.workers.dev/increment?name=${durableObjectName}`, { 90 | method: 'POST', 91 | }); 92 | fetchCount(); // Update count after increment 93 | } catch (error) { 94 | console.error('Error incrementing count:', error); 95 | } 96 | }; 97 | 98 | const submitScore = async (scoreData, category) => { 99 | const dataWithCategory = { 100 | ...scoreData, 101 | category, 102 | }; 103 | 104 | try { 105 | const response = await fetch('https://colormatch.adam-f8f.workers.dev/submit-score', { 106 | method: 'POST', 107 | headers: { 108 | 'Content-Type': 'application/json', 109 | }, 110 | body: JSON.stringify(dataWithCategory), 111 | }); 112 | 113 | if (!response.ok) { 114 | throw new Error(`HTTP error! status: ${response.status}`); 115 | } 116 | 117 | console.log("Score submitted successfully"); 118 | return true; // Indicate success 119 | } catch (error) { 120 | console.error("Failed to submit score:", error); 121 | return false; // Indicate failure 122 | } 123 | }; 124 | 125 | const fetchScores = async (category) => { 126 | // The category should be either 'daily' or 'all-time' 127 | const url = `https://colormatch.adam-f8f.workers.dev/get-scores?category=${category}`; 128 | 129 | try { 130 | const response = await fetch(url); 131 | if (!response.ok) { 132 | throw new Error(`HTTP error! status: ${response.status}`); 133 | } 134 | const scores = await response.json(); 135 | return scores; 136 | } catch (error) { 137 | console.error("Failed to fetch scores:", error); 138 | return []; 139 | } 140 | }; 141 | 142 | // Moved loadScores outside useEffect and wrapped in useCallback 143 | const loadScores = useCallback(async () => { 144 | console.log("Loading scores..."); 145 | try { 146 | const fetchedDailyScores = await fetchScores('daily'); 147 | const fetchedAllTimeScores = await fetchScores('all-time'); 148 | setDailyScores(fetchedDailyScores); 149 | setAllTimeScores(fetchedAllTimeScores); 150 | console.log("Scores loaded."); 151 | } catch (error) { 152 | console.error("Error loading scores:", error); 153 | // Handle error appropriately, maybe set scores to empty array or show message 154 | setDailyScores([]); 155 | setAllTimeScores([]); 156 | } 157 | }, []); // Empty dependency array if fetchScores is stable or defined outside 158 | 159 | useEffect(() => { 160 | localStorage.setItem('gameHistory', JSON.stringify(gameHistory)); 161 | fetchCount(); 162 | }, [gameHistory]); 163 | 164 | useEffect(() => { 165 | // Call loadScores on initial mount 166 | loadScores(); 167 | }, [loadScores]); // Depend on the useCallback version of loadScores 168 | 169 | 170 | // Start a new game 171 | const startNewGame = () => { 172 | const newRandomColor = chroma.random().hex(); 173 | setRandomColor(newRandomColor); 174 | if (timerId) { 175 | clearInterval(timerId); 176 | setTimerId(null); 177 | } 178 | setSelectedColor('#F0F0F0'); 179 | setStartTime(new Date().getTime()); 180 | setScore(null); 181 | setTimeTaken(0); 182 | setAccuracy(''); 183 | setShowPlayAgain(false); 184 | setElapsedTime(0); 185 | setPixelPerfectBadge(false) 186 | const id = setInterval(() => { 187 | setElapsedTime((prevTime) => prevTime + 10); 188 | }, 10); 189 | setTimerId(id); 190 | }; 191 | 192 | // Handle color change from color picker 193 | const handleColorChange = (color) => { 194 | setSelectedColor(color.hex); 195 | }; 196 | 197 | 198 | // const handleColorSelection = (color) => { 199 | // setSelectedColor(color); 200 | // }; 201 | 202 | // Submit the selected color and calculate the score 203 | // Make handleSubmit async 204 | const handleSubmit = useCallback(async () => { 205 | if (timerId) { 206 | clearInterval(timerId); 207 | setTimerId(null); 208 | } 209 | 210 | handleIncrement() 211 | 212 | let currentUsername = username; // Use a local variable 213 | if (!currentUsername) { 214 | const enteredUsername = prompt('Please enter your username:', 'Anonymous'); 215 | if (enteredUsername) { 216 | setUsername(enteredUsername); 217 | localStorage.setItem('username', enteredUsername); 218 | currentUsername = enteredUsername; // Update local variable 219 | } else { 220 | return; // Prevent further action 221 | } 222 | } 223 | 224 | const endTime = Date.now(); // Use Date.now() for consistency 225 | const timeInSeconds = (endTime - startTime) / 1000; 226 | // Ensure accuracy is >= 0 227 | const calculatedAccuracy = Math.max(0, 100 - chroma.deltaE(randomColor, selectedColor)); 228 | // Ensure score isn't negative due to time penalty 229 | let combinedScore = Math.max(0, (calculatedAccuracy + Math.max(0, 100 - timeInSeconds * 2)) / 2); 230 | 231 | if (timeInSeconds < 0.5) { 232 | combinedScore *= 0.5; 233 | console.log("Potential computer aid detected, score penalized."); 234 | } 235 | 236 | setScore(combinedScore); // Set score as number 237 | setTimeTaken(timeInSeconds); 238 | setAccuracy(calculatedAccuracy); 239 | 240 | // Update game history 241 | const newHistoryEntry = { score: combinedScore, timeTaken: timeInSeconds, accuracy: calculatedAccuracy, targetColor: randomColor, guessedColor: selectedColor, timestamp: endTime }; 242 | const updatedHistory = [newHistoryEntry, ...gameHistory].slice(0, 200); 243 | setGameHistory(updatedHistory); 244 | 245 | setShowPlayAgain(true); 246 | 247 | let isNewHighScore = false; 248 | if (combinedScore > highScore) { 249 | const scoreImprovement = combinedScore - highScore; 250 | setHighScore(combinedScore); 251 | localStorage.setItem('highScore', combinedScore.toString()); 252 | toast.success(`🚀 New high score: ${combinedScore.toFixed(3)}! (+${scoreImprovement.toFixed(3)})`); 253 | isNewHighScore = true; 254 | } 255 | 256 | // Prepare score data 257 | const scoreData = { 258 | user: currentUsername, // Use the potentially updated username 259 | score: combinedScore, 260 | accuracy: calculatedAccuracy, 261 | timeTaken: timeInSeconds, 262 | selectedColor: selectedColor, 263 | randomColor: randomColor, 264 | mode: 'single' // Assuming 'single' mode 265 | }; 266 | 267 | // Await score submission 268 | console.log("Submitting score..."); 269 | const submitted = await submitScore(scoreData, 'daily'); 270 | 271 | // If submission was successful, refresh the leaderboards 272 | if (submitted) { 273 | console.log("Submission successful, reloading scores..."); 274 | await loadScores(); // Call the moved loadScores function 275 | } else { 276 | console.log("Submission failed, not reloading scores."); 277 | // Optionally show an error toast to the user 278 | toast.error("Failed to submit score. Leaderboards may not be up-to-date."); 279 | } 280 | 281 | // Handle pixel perfect badge separately if needed 282 | if (calculatedAccuracy === 100) { 283 | // setPixelPerfectBadge(true); // This state seems removed, adjust if needed 284 | console.log("Perfect accuracy achieved!"); 285 | } 286 | 287 | }, [timerId, username, startTime, randomColor, selectedColor, gameHistory, highScore, handleIncrement, submitScore, loadScores, setUsername, setGameHistory]); // Add dependencies 288 | 289 | const handleColorInputOpen = () => { 290 | if (!timerId) { // Start the timer only if it's not already running 291 | const id = setInterval(() => { 292 | setElapsedTime((prevTime) => prevTime + 10); 293 | }, 10); // Timer updates every 10 milliseconds 294 | setTimerId(id); 295 | } 296 | }; 297 | 298 | // Calculate average values 299 | const calculateAverages = () => { 300 | if (gameHistory.length === 0) { 301 | return { avgScore: 'N/A', avgAccuracy: 'N/A', avgTime: 'N/A' }; 302 | } 303 | 304 | const total = gameHistory.reduce( 305 | (acc, game) => { 306 | acc.totalScore += game.score; 307 | acc.totalAccuracy += game.accuracy; 308 | acc.totalTime += game.timeTaken; // Use timeTaken directly as it's a number 309 | return acc; 310 | }, 311 | { totalScore: 0, totalAccuracy: 0, totalTime: 0 } 312 | ); 313 | 314 | const numGames = gameHistory.length; 315 | return { 316 | avgScore: (total.totalScore / numGames).toFixed(3), 317 | avgAccuracy: (total.totalAccuracy / numGames).toFixed(3) + '%', 318 | avgTime: (total.totalTime / numGames).toFixed(3) + ' seconds' 319 | }; 320 | }; 321 | 322 | const averages = calculateAverages(); 323 | 324 | useEffect(() => { 325 | const handleEsc = (event) => { 326 | if (event.keyCode === 27) { 327 | setShowDailyTop100Modal(false); 328 | setShowAllTimeTop100Modal(false); 329 | } 330 | }; 331 | window.addEventListener('keydown', handleEsc); 332 | 333 | return () => { 334 | window.removeEventListener('keydown', handleEsc); 335 | }; 336 | }, []); 337 | 338 | useEffect(() => { 339 | startNewGame(); // Start a game when the component mounts 340 | }, []); 341 | 342 | useEffect(() => { 343 | // Function to handle key press 344 | const handleKeyPress = (event) => { 345 | if (event.key === 'Enter') { 346 | handleSubmit(); // Call your existing submit function 347 | } 348 | }; 349 | 350 | // Add event listener 351 | document.addEventListener('keydown', handleKeyPress); 352 | 353 | // Remove event listener on cleanup 354 | return () => { 355 | document.removeEventListener('keydown', handleKeyPress); 356 | }; 357 | }, [handleSubmit]); 358 | 359 | return ( 360 |
364 |
365 | {randomColor &&
369 |
4? 'white' : 'black', 375 | display: 'flex', alignItems: 'center', justifyContent: 'center' 376 | }} 377 | > 378 | {!showPlayAgain ? ( 379 |
380 |
381 |
Time
382 |
{(elapsedTime / 1000).toFixed(3)} seconds
383 |
{((new Date().getTime() - startTime) / 1000).toFixed(3)} seconds
384 |
385 | 386 | 387 | 388 | 389 |
390 | ) : ( 391 |
392 | {score &&
393 | 394 |
395 | 4.5? 'white' : 'black', color: randomColor }}>{randomColor} 396 | {selectedColor} 397 |
398 | 399 |
400 |
Score
401 |
{score ? Number(score).toFixed(3) : '...'}
402 |
403 |
404 |
405 |
Time
406 |
407 | {timeTaken ? Number(timeTaken).toFixed(3) : '...'} seconds 408 |
409 |
410 |
411 |
Accuracy
412 |
{accuracy ? Number(accuracy).toFixed(3) : '...'}%
413 |
414 |
415 |
416 | } 417 | 418 |
419 | { 420 | dailyScores && dailyScores.length > 0 && score > dailyScores[dailyScores.length - 1].score && ( 421 | 422 | 🎪 Daily Top 100 423 | 424 | ) 425 | } 426 | { 427 | allTimeScores && allTimeScores.length > 0 && score > allTimeScores[allTimeScores.length - 1].score && ( 428 | 429 | 💎 All-time 100 !!! 430 | 431 | ) 432 | } 433 | {accuracy >= 95 && 434 | 435 | 🔭 Super vision lvl 3 436 | 437 | } 438 | {timeTaken <= 1 && 439 | 440 | ⚡ Lightning fast 441 | 442 | } 443 |
444 |
445 | )} 446 |
447 |
459 |
460 | 467 | {!showPlayAgain ? ( 468 |
469 | 475 |
476 | ) : ( 477 |
4? 'white' : 'black', 479 | }} className='db dn-ns'> 480 |

High Scores

481 |

Today

482 |
    483 | {dailyScores.slice(0,10).map((score, index) => { 484 | const isCurrentUser = username && score.user === username; 485 | const liStyle = { 486 | color: 'inherit', 487 | fontSize: '10px', 488 | minWidth: '192px', 489 | padding: '2px 4px', // Added horizontal padding 490 | margin: '0 -4px', // Counteract padding for alignment 491 | borderRadius: '3px', // Rounded corners for highlight 492 | borderBottom: '1px solid', 493 | display: 'flex', 494 | alignItems: 'center', 495 | justifyContent: 'space-between', 496 | gap: '32px', 497 | backgroundColor: isCurrentUser ? 'rgba(255, 255, 0, 0.2)' : 'transparent', // Subtle yellow highlight 498 | fontWeight: isCurrentUser ? 'bold' : 'normal' // Bold for current user 499 | }; 500 | return ( 501 |
  1. 502 | {score.user} 503 | {score.score.toFixed(3)} 504 |
  2. 505 | ); 506 | })} 507 |
508 | 509 | 510 |
511 | )} 512 |
513 |
514 |
} 515 |
516 | {!showPlayAgain ? ( 517 |
518 | 525 |
526 | ) : ( 527 |
528 | 535 |
536 | )} 537 | 538 | 539 |
540 |
542 |
4.5? 'white' : 'black', 544 | }}> 545 |

Today

546 |
    547 | {dailyScores.slice(0,10).map((score, index) => { 548 | const isCurrentUser = username && score.user === username; 549 | const liStyle = { 550 | color: 'inherit', 551 | fontSize: '10px', 552 | minWidth: '192px', 553 | padding: '2px 4px', // Added horizontal padding 554 | margin: '0 -4px', // Counteract padding for alignment 555 | borderRadius: '3px', // Rounded corners for highlight 556 | borderBottom: '1px solid', 557 | display: 'flex', 558 | alignItems: 'center', 559 | justifyContent: 'space-between', 560 | gap: '32px', 561 | backgroundColor: isCurrentUser ? 'rgba(255, 255, 0, 0.2)' : 'transparent', // Subtle yellow highlight 562 | fontWeight: isCurrentUser ? 'bold' : 'normal' // Bold for current user 563 | }; 564 | return ( 565 |
  1. 566 | {score.user} 567 | {score.score.toFixed(3)} 568 |
  2. 569 | ); 570 | })} 571 |
572 | 573 |
574 | 575 |
4.5? 'white' : 'black', backgroundColor: selectedColor, zIndex: '99' }}> 576 |

All-Time

577 |
    578 | {allTimeScores.slice(0,10).map((score, index) => { 579 | const isCurrentUser = username && score.user === username; 580 | const liStyle = { 581 | fontSize: '10px', 582 | minWidth: '192px', 583 | padding: '2px 4px', 584 | margin: '0 -4px', 585 | borderRadius: '3px', 586 | borderBottom: '1px solid', 587 | display: 'flex', 588 | alignItems: 'center', 589 | justifyContent: 'space-between', 590 | gap: '32px', 591 | backgroundColor: isCurrentUser ? 'rgba(255, 255, 0, 0.2)' : 'transparent', 592 | fontWeight: isCurrentUser ? 'bold' : 'normal' 593 | }; 594 | return ( 595 |
  1. 596 | {score.user} 597 | {score.score.toFixed(3)} 598 |
  2. 599 | ); 600 | })} 601 |
602 | 603 |
4.5? 'white' : 'black' }}> 604 |

Stats

605 |
606 |

Avg. Score: {averages.avgScore}

607 |

Avg. Accuracy: {averages.avgAccuracy}

608 | {highScore &&

High Score: {parseFloat(highScore).toFixed(3)}

} 609 |
610 |
611 |
612 |
613 | 4? 'white' : 'black', 615 | }}>{count} plays 616 |
617 |
618 | 619 |
620 | 621 | {/* ... rest of your component */} 622 |
623 | setShowDailyTop100Modal(false)} 626 | scores={dailyScores} 627 | title="Top 100 Daily Scores" 628 | currentUser={username} 629 | /> 630 | setShowAllTimeTop100Modal(false)} 633 | scores={allTimeScores} 634 | title="Top 100 All-Time Scores" 635 | currentUser={username} 636 | /> 637 |
638 | ); 639 | } 640 | 641 | export default App; 642 | --------------------------------------------------------------------------------