├── 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 | - {score.user}
{score.score.toFixed(3)}
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 | -
502 | {score.user}
503 |
{score.score.toFixed(3)}
504 |
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 |
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 |
--------------------------------------------------------------------------------