├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── Helper.js ├── components └── ItemList.js ├── index.js ├── serviceWorker.js └── setupTests.js /.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 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Typing-speed Test Game 2 | 3 | 1 minute Typing speed test game built with React. Test your typing speed online and find out how fast can you type in real world. 4 | 5 | 6 | ## [Website](https://typingspeedtest.now.sh/) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speed-typing-test", 3 | "version": "0.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-scripts": "3.3.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ ">0.2%", "not dead", "not op_mini all" ], 24 | "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awran5/react-typing-speed-test-game/8a2ea2ead855af48d4f1d6d3412066b10443216d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 16 | 17 | 26 | Typing Speed Test Game 27 | 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awran5/react-typing-speed-test-game/8a2ea2ead855af48d4f1d6d3412066b10443216d/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awran5/react-typing-speed-test-game/8a2ea2ead855af48d4f1d6d3412066b10443216d/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::after, 3 | ::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, 9 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 10 | font-size: 1rem; 11 | font-weight: 400; 12 | line-height: 1.5; 13 | color: rgb(33, 37, 41); 14 | text-align: left; 15 | background-color: rgb(255, 255, 255); 16 | margin: 0px; 17 | } 18 | 19 | a { 20 | color: rgb(0, 123, 255); 21 | background-color: transparent; 22 | text-decoration: none; 23 | } 24 | 25 | .h1, 26 | .h2, 27 | .h3, 28 | .h4, 29 | .h5, 30 | .h6, 31 | h1, 32 | h2, 33 | h3, 34 | h4, 35 | h5, 36 | h6 { 37 | margin-bottom: 0.5rem; 38 | font-weight: 500; 39 | line-height: 1.2; 40 | } 41 | h1, 42 | h2, 43 | h3, 44 | h4, 45 | h5, 46 | h6 { 47 | margin-top: 0px; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | .h6, 52 | h6 { 53 | font-size: 1rem; 54 | } 55 | 56 | .bg-dark { 57 | background-color: rgb(35, 38, 41) !important; 58 | } 59 | 60 | .container-fluid, 61 | .container-lg, 62 | .container-md, 63 | .container-sm, 64 | .container-xl { 65 | width: 100%; 66 | padding-right: 15px; 67 | padding-left: 15px; 68 | margin-right: auto; 69 | margin-left: auto; 70 | } 71 | 72 | .row { 73 | display: flex; 74 | flex-wrap: wrap; 75 | margin-right: -15px; 76 | margin-left: -15px; 77 | } 78 | 79 | .col, 80 | .col-1, 81 | .col-10, 82 | .col-11, 83 | .col-12, 84 | .col-2, 85 | .col-3, 86 | .col-4, 87 | .col-5, 88 | .col-6, 89 | .col-7, 90 | .col-8, 91 | .col-9, 92 | .col-auto, 93 | .col-lg, 94 | .col-lg-1, 95 | .col-lg-10, 96 | .col-lg-11, 97 | .col-lg-12, 98 | .col-lg-2, 99 | .col-lg-3, 100 | .col-lg-4, 101 | .col-lg-5, 102 | .col-lg-6, 103 | .col-lg-7, 104 | .col-lg-8, 105 | .col-lg-9, 106 | .col-lg-auto, 107 | .col-md, 108 | .col-md-1, 109 | .col-md-10, 110 | .col-md-11, 111 | .col-md-12, 112 | .col-md-2, 113 | .col-md-3, 114 | .col-md-4, 115 | .col-md-5, 116 | .col-md-6, 117 | .col-md-7, 118 | .col-md-8, 119 | .col-md-9, 120 | .col-md-auto, 121 | .col-sm, 122 | .col-sm-1, 123 | .col-sm-10, 124 | .col-sm-11, 125 | .col-sm-12, 126 | .col-sm-2, 127 | .col-sm-3, 128 | .col-sm-4, 129 | .col-sm-5, 130 | .col-sm-6, 131 | .col-sm-7, 132 | .col-sm-8, 133 | .col-sm-9, 134 | .col-sm-auto, 135 | .col-xl, 136 | .col-xl-1, 137 | .col-xl-10, 138 | .col-xl-11, 139 | .col-xl-12, 140 | .col-xl-2, 141 | .col-xl-3, 142 | .col-xl-4, 143 | .col-xl-5, 144 | .col-xl-6, 145 | .col-xl-7, 146 | .col-xl-8, 147 | .col-xl-9, 148 | .col-xl-auto { 149 | position: relative; 150 | width: 100%; 151 | padding-right: 15px; 152 | padding-left: 15px; 153 | } 154 | 155 | .col { 156 | flex-basis: 0px; 157 | flex-grow: 1; 158 | max-width: 100%; 159 | } 160 | 161 | .display-4 { 162 | font-size: 3.5rem; 163 | font-weight: 300; 164 | line-height: 1.2; 165 | } 166 | .d-flex { 167 | display: flex !important; 168 | } 169 | .d-block { 170 | display: block !important; 171 | } 172 | .pb-2, 173 | .py-2 { 174 | padding-bottom: 0.5rem !important; 175 | } 176 | 177 | .pt-2, 178 | .py-2 { 179 | padding-top: 0.5rem !important; 180 | } 181 | .pt-3, 182 | .py-3 { 183 | padding-top: 1rem !important; 184 | } 185 | 186 | .pt-4, 187 | .py-4 { 188 | padding-top: 1.5rem !important; 189 | } 190 | 191 | .pb-4, 192 | .py-4 { 193 | padding-bottom: 1.5rem !important; 194 | } 195 | 196 | .p-4 { 197 | padding: 1.5rem !important; 198 | } 199 | 200 | .pl-5, 201 | .px-5 { 202 | padding-left: 3rem !important; 203 | } 204 | .pr-5, 205 | .px-5 { 206 | padding-right: 3rem !important; 207 | } 208 | 209 | .text-center { 210 | text-align: center !important; 211 | } 212 | 213 | .m-1 { 214 | margin: 0.25rem !important; 215 | } 216 | 217 | .mb-1, 218 | .my-1 { 219 | margin-bottom: 0.25rem !important; 220 | } 221 | 222 | .mt-1, 223 | .my-1 { 224 | margin-top: 0.25rem !important; 225 | } 226 | 227 | .mt-4, 228 | .my-4 { 229 | margin-top: 1.5rem !important; 230 | } 231 | 232 | .mb-5, 233 | .my-5 { 234 | margin-bottom: 3rem !important; 235 | } 236 | 237 | .mt-5, 238 | .my-5 { 239 | margin-top: 3rem !important; 240 | } 241 | 242 | .lead { 243 | font-size: 1.25rem; 244 | font-weight: 300; 245 | } 246 | 247 | .w-50 { 248 | width: 50% !important; 249 | } 250 | .text-danger { 251 | color: #dc3545 !important; 252 | } 253 | .text-muted { 254 | color: rgb(108, 117, 125) !important; 255 | } 256 | 257 | .text-light { 258 | color: rgb(248, 249, 250) !important; 259 | } 260 | .text-white { 261 | color: rgb(255, 255, 250) !important; 262 | } 263 | 264 | .rounded { 265 | border-radius: 0.25rem !important; 266 | } 267 | 268 | .border { 269 | border-width: 1px !important; 270 | border-style: solid !important; 271 | border-color: rgb(222, 226, 230) !important; 272 | border-image: initial !important; 273 | } 274 | 275 | hr { 276 | margin-top: 1rem; 277 | margin-bottom: 1rem; 278 | border-right-style: initial; 279 | border-bottom-style: initial; 280 | border-left-style: initial; 281 | border-right-color: initial; 282 | border-bottom-color: initial; 283 | border-left-color: initial; 284 | border-width: 1px 0px 0px; 285 | border-image: initial; 286 | border-top: 1px solid rgba(0, 0, 0, 0.1); 287 | } 288 | 289 | hr { 290 | box-sizing: content-box; 291 | height: 0px; 292 | overflow: visible; 293 | } 294 | 295 | .alert-warning { 296 | color: #856404; 297 | background-color: #fff3cd; 298 | border-color: #ffeeba; 299 | } 300 | .alert-danger { 301 | color: rgb(185, 74, 72); 302 | border: 1px solid rgb(238, 211, 215); 303 | text-align: left; 304 | width: 90%; 305 | margin: 0 auto; 306 | font-size: 14px; 307 | } 308 | 309 | .alert { 310 | position: relative; 311 | padding: .75rem 1.25rem; 312 | margin-bottom: 1rem; 313 | border-radius: .25rem; 314 | } 315 | 316 | .list-unstyled { 317 | padding-left: 0px; 318 | list-style: none; 319 | } 320 | .list-inline { 321 | padding-left: 0px; 322 | list-style: none; 323 | } 324 | 325 | .list-inline-item:not(:last-child) { 326 | margin-right: 0.5rem; 327 | } 328 | 329 | .list-inline-item { 330 | display: inline-block; 331 | } 332 | 333 | .small, 334 | small { 335 | font-size: 80%; 336 | font-weight: 400; 337 | } 338 | 339 | [type="button"]:not(:disabled), 340 | [type="reset"]:not(:disabled), 341 | [type="submit"]:not(:disabled), 342 | button:not(:disabled) { 343 | cursor: pointer; 344 | } 345 | [type="button"], 346 | [type="reset"], 347 | [type="submit"], 348 | button { 349 | -webkit-appearance: button; 350 | } 351 | 352 | @media (prefers-reduced-motion: reduce) { 353 | .btn { 354 | transition: none 0s ease 0s; 355 | } 356 | } 357 | 358 | .btn { 359 | display: inline-block; 360 | font-weight: 400; 361 | color: rgb(33, 37, 41); 362 | text-align: center; 363 | vertical-align: middle; 364 | cursor: pointer; 365 | user-select: none; 366 | background-color: transparent; 367 | font-size: 1rem; 368 | line-height: 1.5; 369 | border-width: 1px; 370 | border-style: solid; 371 | border-color: transparent; 372 | border-image: initial; 373 | padding: 0.375rem 0.75rem; 374 | border-radius: 0.25rem; 375 | transition: color 0.15s ease-in-out 0s, background-color 0.15s ease-in-out 0s, border-color 0.15s ease-in-out 0s, 376 | box-shadow 0.15s ease-in-out 0s; 377 | } 378 | 379 | .btn-outline-success { 380 | color: #28a745; 381 | border-color: #28a745; 382 | } 383 | 384 | .btn-outline-success:hover { 385 | color: #fff; 386 | background-color: #28a745; 387 | border-color: #28a745; 388 | } 389 | .btn-outline-success:focus, 390 | .btn-outline-success.focus { 391 | box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); 392 | } 393 | 394 | .btn-outline-success.disabled, 395 | .btn-outline-success:disabled { 396 | color: #28a745; 397 | background-color: transparent; 398 | } 399 | 400 | .btn-outline-success:not(:disabled):not(.disabled):active, 401 | .btn-outline-success:not(:disabled):not(.disabled).active, 402 | .show > .btn-outline-success.dropdown-toggle { 403 | color: #fff; 404 | background-color: #28a745; 405 | border-color: #28a745; 406 | } 407 | 408 | .btn-outline-success:not(:disabled):not(.disabled):active:focus, 409 | .btn-outline-success:not(:disabled):not(.disabled).active:focus, 410 | .show > .btn-outline-success.dropdown-toggle:focus { 411 | box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); 412 | } 413 | 414 | .btn-outline-danger { 415 | color: rgb(220, 53, 69); 416 | border-color: rgb(220, 53, 69); 417 | } 418 | 419 | .btn-outline-danger:hover { 420 | color: #fff; 421 | background-color: rgb(185, 74, 72); 422 | border-color: rgb(185, 74, 72); 423 | } 424 | 425 | .btn-outline-danger:focus, 426 | .btn-outline-danger.focus { 427 | box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); 428 | } 429 | 430 | .btn-outline-danger.disabled, 431 | .btn-outline-danger:disabled { 432 | color: rgb(185, 74, 72); 433 | background-color: transparent; 434 | } 435 | 436 | .btn-outline-danger:not(:disabled):not(.disabled):active, 437 | .btn-outline-danger:not(:disabled):not(.disabled).active, 438 | .show > .btn-outline-danger.dropdown-toggle { 439 | color: #fff; 440 | background-color: rgb(185, 74, 72); 441 | border-color: rgb(185, 74, 72); 442 | } 443 | 444 | .btn-outline-danger:not(:disabled):not(.disabled):active:focus, 445 | .btn-outline-danger:not(:disabled):not(.disabled).active:focus, 446 | .show > .btn-outline-danger.dropdown-toggle:focus { 447 | box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); 448 | } 449 | 450 | @media (min-width: 576px) { 451 | .container { 452 | max-width: 540px; 453 | } 454 | .col-sm-6 { 455 | max-width: 50%; 456 | flex: 0 0 50%; 457 | } 458 | .col-sm-12 { 459 | max-width: 100%; 460 | flex: 0 0 100%; 461 | } 462 | } 463 | 464 | @media (min-width: 768px) { 465 | .container { 466 | max-width: 720px; 467 | } 468 | .col-md-2 { 469 | max-width: 16.6667%; 470 | flex: 0 0 16.6667%; 471 | } 472 | .col-md-8 { 473 | max-width: 66.6667%; 474 | flex: 0 0 66.6667%; 475 | } 476 | .order-md-1 { 477 | order: 1; 478 | } 479 | .order-md-2 { 480 | order: 2; 481 | } 482 | } 483 | @media (min-width: 992px) { 484 | .container { 485 | max-width: 960px; 486 | } 487 | } 488 | @media (min-width: 1200px) { 489 | .container { 490 | max-width: 1140px; 491 | } 492 | } 493 | 494 | /* Custom */ 495 | .btn-circle { 496 | border-radius: 50%; 497 | border-width: 2px; 498 | width: 70px; 499 | height: 70px; 500 | padding: 0; 501 | font-weight: bold; 502 | position: relative; 503 | animation: border-pulse 2s infinite; 504 | } 505 | .btn-circle:focus { 506 | outline: 0; 507 | } 508 | 509 | @keyframes border-pulse { 510 | 0% { 511 | box-shadow: 0px 0px 15px 0px rgba(138, 33, 33, 0.575); 512 | } 513 | 50% { 514 | box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0); 515 | } 516 | 100% { 517 | box-shadow: 0px 0px 15px 0px rgba(138, 33, 33, 0.575); 518 | } 519 | } 520 | 521 | @keyframes flashing { 522 | 0% { 523 | -webkit-transform: scale(0); 524 | transform: scale(0); 525 | 526 | opacity: 0.0; 527 | } 528 | 20% { 529 | -webkit-transform: scale(0.2); 530 | transform: scale(0.2); 531 | opacity: 0.2; 532 | } 533 | 40% { 534 | -webkit-transform: scale(0.5); 535 | transform: scale(0.5); 536 | opacity: 0.5; 537 | } 538 | 60% { 539 | -webkit-transform: scale(0.7); 540 | transform: scale(0.7); 541 | opacity: 0.7; 542 | } 543 | 80% { 544 | -webkit-transform: scale(0.9); 545 | transform: scale(0.9); 546 | background-color: rgba(85, 85, 85, 0.247); 547 | opacity: 0.2; 548 | } 549 | 100% { 550 | -webkit-transform: scale(1.1); 551 | transform: scale(1.1); 552 | opacity: 0.0; 553 | } 554 | } 555 | 556 | .mono { 557 | font-family: "Cutive Mono", "Courier New", monospace; 558 | } 559 | .quotes { 560 | background-color: #222; 561 | font-size: 2.75rem; 562 | white-space: pre; 563 | padding: .75rem 1rem; 564 | overflow: hidden; 565 | display: inline-block; 566 | width: 100%; 567 | min-height: 5.75rem; 568 | border-radius: .25rem; 569 | position: relative; 570 | } 571 | 572 | .quotes.active { 573 | box-shadow: 0 0 3px 0.15rem rgb(23, 168, 27); 574 | outline: 0; 575 | } 576 | 577 | .quotes.active.is-error { 578 | box-shadow: 0 0 3px 0.15rem rgb(190, 25, 25); 579 | outline: 0; 580 | } 581 | 582 | .text-output { 583 | padding: 1.5rem; 584 | max-width: 600px; 585 | margin: 2rem auto; 586 | min-height: 100px; 587 | border-radius: .25rem; 588 | } 589 | 590 | .quotes.active::after { 591 | content: ''; 592 | position: absolute; 593 | width: 1.25rem; 594 | height: .3rem; 595 | background: #fff; 596 | left: 1.20rem; 597 | top: 4.25rem; 598 | animation: flash .2s infinite; 599 | -webkit-animation: flash .2s infinite; 600 | } 601 | 602 | .meter-gauge > span { 603 | text-align: center; 604 | font-size: 12px; 605 | min-height: 25px; 606 | padding: 5px; 607 | } 608 | 609 | .list-item { 610 | box-shadow: 1px 1px 3px rgba(51, 51, 51, 0.2); 611 | } 612 | 613 | @-webkit-keyframes flash { 614 | 0% { 615 | background: #fff; 616 | } 617 | 618 | 50% { 619 | background: #ff9999; 620 | } 621 | 622 | 100% { 623 | background: #fff; 624 | } 625 | } 626 | 627 | @keyframes flash { 628 | 0% { 629 | background: #fff; 630 | } 631 | 632 | 50% { 633 | background: #ff9999; 634 | } 635 | 636 | 100% { 637 | background: #fff; 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react' 2 | import { quotesArray, random, allowedKeys } from './Helper' 3 | import ItemList from './components/ItemList' 4 | import './App.css' 5 | 6 | let interval = null 7 | 8 | const App = () => { 9 | const inputRef = useRef(null) 10 | const outputRef = useRef(null) 11 | const [ duration, setDuration ] = useState(60) 12 | const [ started, setStarted ] = useState(false) 13 | const [ ended, setEnded ] = useState(false) 14 | const [ index, setIndex ] = useState(0) 15 | const [ correctIndex, setCorrectIndex ] = useState(0) 16 | const [ errorIndex, setErrorIndex ] = useState(0) 17 | const [ quote, setQuote ] = useState({}) 18 | const [ input, setInput ] = useState('') 19 | const [ cpm, setCpm ] = useState(0) 20 | const [ wpm, setWpm ] = useState(0) 21 | const [ accuracy, setAccuracy ] = useState(0) 22 | const [ isError, setIsError ] = useState(false) 23 | const [ lastScore, setLastScore ] = useState('0') 24 | 25 | useEffect(() => { 26 | const newQuote = random(quotesArray) 27 | setQuote(newQuote) 28 | setInput(newQuote.quote) 29 | }, []) 30 | 31 | const handleEnd = () => { 32 | setEnded(true) 33 | setStarted(false) 34 | clearInterval(interval) 35 | } 36 | 37 | const setTimer = () => { 38 | const now = Date.now() 39 | const seconds = now + duration * 1000 40 | interval = setInterval(() => { 41 | const secondLeft = Math.round((seconds - Date.now()) / 1000) 42 | setDuration(secondLeft) 43 | if (secondLeft === 0) { 44 | handleEnd() 45 | } 46 | }, 1000) 47 | } 48 | 49 | const handleStart = () => { 50 | setStarted(true) 51 | setEnded(false) 52 | setInput(quote.quote) 53 | inputRef.current.focus() 54 | setTimer() 55 | } 56 | 57 | const handleKeyDown = e => { 58 | e.preventDefault() 59 | const { key } = e 60 | const quoteText = quote.quote 61 | 62 | if (key === quoteText.charAt(index)) { 63 | setIndex(index + 1) 64 | const currenChar = quoteText.substring(index + 1, index + quoteText.length) 65 | setInput(currenChar) 66 | setCorrectIndex(correctIndex + 1) 67 | setIsError(false) 68 | outputRef.current.innerHTML += key 69 | } else { 70 | if (allowedKeys.includes(key)) { 71 | setErrorIndex(errorIndex + 1) 72 | setIsError(true) 73 | outputRef.current.innerHTML += `${key}` 74 | } 75 | } 76 | 77 | const timeRemains = ((60 - duration) / 60).toFixed(2) 78 | const _accuracy = Math.floor((index - errorIndex) / index * 100) 79 | const _wpm = Math.round(correctIndex / 5 / timeRemains) 80 | 81 | if (index > 5) { 82 | setAccuracy(_accuracy) 83 | setCpm(correctIndex) 84 | setWpm(_wpm) 85 | } 86 | 87 | if (index + 1 === quoteText.length || errorIndex > 50) { 88 | handleEnd() 89 | } 90 | } 91 | 92 | useEffect( 93 | () => { 94 | if (ended) localStorage.setItem('wpm', wpm) 95 | }, 96 | [ ended, wpm ] 97 | ) 98 | useEffect(() => { 99 | const stroedScore = localStorage.getItem('wpm') 100 | if (stroedScore) setLastScore(stroedScore) 101 | }, []) 102 | 103 | return ( 104 |
105 |
106 |
107 | {/* Left */} 108 |
109 |
    110 | 0 && wpm < 20 ? ( 115 | { color: 'white', backgroundColor: '#eb4841' } 116 | ) : wpm >= 20 && wpm < 40 ? ( 117 | { color: 'white', backgroundColor: '#f48847' } 118 | ) : wpm >= 40 && wpm < 60 ? ( 119 | { color: 'white', backgroundColor: '#ffc84a' } 120 | ) : wpm >= 60 && wpm < 80 ? ( 121 | { color: 'white', backgroundColor: '#a6c34c' } 122 | ) : wpm >= 80 ? ( 123 | { color: 'white', backgroundColor: '#4ec04e' } 124 | ) : ( 125 | {} 126 | ) 127 | } 128 | /> 129 | 130 | 131 |
132 |
133 | {/* Body */} 134 |
135 |
136 |
137 |

How Fast Can You Type?

138 |

139 | Start the one-minute Typing speed test and find out how fast can you type in real 140 | world! 141 |

142 | 143 |
144 | Just start typing and don't use backspace to correct your mistakes. Your 145 | mistakes will be marked in Red color and shown below the writing box. Good 146 | luck! 147 |
148 | 149 |
150 | {ended ? ( 151 | 157 | ) : started ? ( 158 | 161 | ) : ( 162 | 165 | )} 166 | 167 |
168 |
169 | 170 | {ended ? ( 171 |
172 | "{quote.quote}" 173 | - {quote.author} 174 |
175 | ) : started ? ( 176 |
184 | {input} 185 |
186 | ) : ( 187 |
188 | {input} 189 |
190 | )} 191 | 192 |
193 | 194 |
Tip!
195 |
    196 |
  • 197 | Word Per Minute (WPM) is measured by calculating how many words you can type in 1 198 | minute. 199 |
  • 200 |
  • Character Per Minute (CPM) calculates how many characters are typed per minute.
  • 201 |
  • 202 | The top typing speed was achieved by{' '} 203 | 208 | Stella Pajunas 209 | {' '} 210 | in 1946, whereas Mrs. Barbara Blackburn has averaged 150 wpm in 50 minutes and her 211 | top speed was 212 wpm. 212 |
  • 213 |
214 |
215 |
216 |
Average Typing Speeds
217 |
218 | 219 | 0 - 20 Slow 220 | 221 | 222 | 20 - 40 Average 223 | 224 | 225 | 40 - 60 Fast 226 | 227 | 228 | 60 - 80 Professional 229 | 230 | 231 | 80 - 100+ Top 232 | 233 |
234 |
235 |
236 |
237 | 238 |
239 |
    240 | 241 | 242 | 243 |
244 |
245 |
246 | 247 | 288 |
289 |
290 | ) 291 | } 292 | 293 | export default App 294 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Helper.js: -------------------------------------------------------------------------------- 1 | export const quotesArray = [ 2 | { 3 | quote: 4 | "If we listened to our intellect we'd never have a love affair. We'd never have a friendship. We'd never go in business because we'd be cynical: \"It's gonna go wrong.\" Or \"She's going to hurt me.\" Or,\"I've had a couple of bad love affairs, so therefore . . .\" Well, that's nonsense. You're going to miss life. You've got to jump off the cliff all the time and build your wings on the way down.", 5 | author: 'Ray Bradbury' 6 | }, 7 | { 8 | quote: 9 | "To-morrow, and to-morrow, and to-morrow, Creeps in this petty pace from day to day,To the last syllable of recorded time; And all our yesterdays have lighted fools The way to dusty death. Out, out, brief candle! Life's but a walking shadow, a poor player, That struts and frets his hour upon the stage, And then is heard no more. It is a tale Told by an idiot, full of sound and fury, Signifying nothing.", 10 | author: 'William Shakespeare' 11 | }, 12 | { 13 | quote: 14 | "If a man is called to be a street sweeper, he should sweep streets even as a Michaelangelo painted, or Beethoven composed music or Shakespeare wrote poetry. He should sweep streets so well that all the hosts of heaven and earth will pause to say, 'Here lived a great street sweeper who did his job well.", 15 | author: 'Martin Luther King' 16 | }, 17 | { 18 | quote: 19 | 'Owning our story can be hard but not nearly as difficult as spending our lives running from it. Embracing our vulnerabilities is risky but not nearly as dangerous as giving up on love and belonging and joy—the experiences that make us the most vulnerable. Only when we are brave enough to explore the darkness will we discover the infinite power of our light.', 20 | author: 'Brene Brown' 21 | }, 22 | { 23 | quote: 24 | 'Every day, think as you wake up, today I am fortunate to be alive, I have a precious human life, I am not going to waste it. I am going to use all my energies to develop myself, to expand my heart out to others; to achieve enlightenment for the benefit of all beings. I am going to have kind thoughts towards others, I am not going to get angry or think badly about others. I am going to benefit others as much as I can.', 25 | author: 'The Dalai Lama' 26 | }, 27 | { 28 | quote: 29 | "Life is too short to waste any amount of time on wondering what other people think about you. In the first place, if they had better things going on in their lives, they wouldn't have the time to sit around and talk about you. What's important to me is not others' opinions of me, but what's important to me is my opinion of myself.", 30 | author: ' C. JoyBell C' 31 | }, 32 | { 33 | quote: 34 | "If you can keep your head when all about you Are losing theirs and blaming it on you, If you can trust yourself when all men doubt you, But make allowance for their doubting too; If you can wait and not be tired by waiting, Or being lied about, don't deal in lies, Or being hated, don't give way to hating, And yet don't look too good, nor talk too wise", 35 | author: 'Rudyard Kipling' 36 | }, 37 | { 38 | quote: 39 | 'A fight is going on inside me," said an old man to his son. "It is a terrible fight between two wolves. One wolf is evil. He is anger, envy, sorrow, regret, greed, arrogance, self-pity, guilt, resentment, inferiority, lies, false pride, superiority, and ego. The other wolf is good. he is joy, peace, love, hope, serenity, humility, kindness, benevolence, empathy, generosity, truth, compassion, and faith. The same fight is going on inside you.', 40 | author: 'Wendy Mass' 41 | }, 42 | { 43 | quote: 44 | 'Whatever you do, you need courage. Whatever course you decide upon, there is always someone to tell you that you are wrong. There are always difficulties arising that tempt you to believe your critics are right. To map out a course of action and follow it to an end requires some of the same courage that a soldier needs. Peace has its victories, but it takes brave men and women to win them.', 45 | author: 'Ralph Waldo Emerson' 46 | }, 47 | { 48 | quote: 49 | 'As long as people have been on this earth, the moon has been a mystery to us. Think about it. She is strong enough to pull the oceans, and when she dies away, she always comes back again. My mama used to tell me Our Lady lived on the moon and that I should dance when her face was bright and hibernate when it was dark.', 50 | author: 'Sue Monk Kidd' 51 | }, 52 | { 53 | quote: 54 | "Every form has its own meaning. Every man creates his meaning and form and goal. Why is it so important - what others have done? Why does it become sacred by the mere fact of not being your own? Why is anyone and everyone right - so long as it's not yourself? Why does the number of those others take the place of truth? Why is truth made a mere matter of arithmetic - and only of addition at that? Why is everything twisted out of all sense to fit everything else? There must be some reason. I don't know. I've never known it. I'd like to understand.", 55 | author: 'Ayn Rand' 56 | }, 57 | { 58 | quote: 59 | "What I like about cooking is that, so long as you follow the recipe exactly, everything always turns out perfect. It's too bad there's no recipe for happiness. Happiness is more like pastry—which is to say that you can take pains to keep cool and not overwork the dough, but if you don't have that certain light touch, your best efforts still fall flat. The work-around is to buy what you need. I'm talking about pastry, not happiness, although money does make things easier all around.", 60 | author: 'Josh Lanyon' 61 | }, 62 | { 63 | quote: 64 | "Sometimes I feel like I don't belong anywhere, & it's gonna take so long for me to get to somewhere, Sometimes I feel so heavy hearted, but I can't explain cuz I'm so guarded. But that's a lonely road to travel, and a heavy load to bear. And it's a long, long way to heaven but I gotta get there Can you send an angel? Can you send me an angel...to guide me.", 65 | author: 'Alicia Keys' 66 | }, 67 | { 68 | quote: 69 | 'A human being is a part of the whole called by us universe, a part limited in time and space. He experiences himself, his thoughts and feeling as something separated from the rest, a kind of optical delusion of his consciousness. This delusion is a kind of prison for us, restricting us to our personal desires and to affection for a few persons nearest to us. Our task must be to free ourselves from this prison by widening our circle of compassion to embrace all living creatures and the whole of nature in its beauty.', 70 | author: 'Albert Einstein' 71 | }, 72 | { 73 | quote: 74 | 'The important thing is not to stop questioning. Curiosity has its own reason for existence. One cannot help but be in awe when he contemplates the mysteries of eternity, of life, of the marvelous structure of reality. It is enough if one tries merely to comprehend a little of this mystery each day.', 75 | author: 'Albert Einstein' 76 | }, 77 | { 78 | quote: 79 | "Pain is a pesky part of being human, I've learned it feels like a stab wound to the heart, something I wish we could all do without, in our lives here. Pain is a sudden hurt that can't be escaped. But then I have also learned that because of pain, I can feel the beauty, tenderness, and freedom of healing. Pain feels like a fast stab wound to the heart. But then healing feels like the wind against your face when you are spreading your wings and flying through the air!", 80 | author: 'C. JoyBell C' 81 | }, 82 | { 83 | quote: 84 | 'All that we are is the result of what we have thought: it is founded on our thoughts and made up of our thoughts. If a man speak or act with an evil thought, suffering follows him as the wheel follows the hoof of the beast that draws the wagon.... If a man speak or act with a good thought, happiness follows him like a shadow that never leaves him.', 85 | author: 'Gautama Buddha' 86 | }, 87 | { 88 | quote: 89 | 'Man often becomes what he believes himself to be. If I keep on saying to myself that I cannot do a certain thing, it is possible that I may end by really becoming incapable of doing it. On the contrary, if I have the belief that I can do it, I shall surely acquire the capacity to do it even if I may not have it at the beginning.', 90 | author: 'Mahatma Gandhi' 91 | }, 92 | { 93 | quote: 94 | 'I said to my soul, be still and wait without hope, for hope would be hope for the wrong thing; wait without love, for love would be love of the wrong thing; there is yet faith, but the faith and the love are all in the waiting. Wait without thought, for you are not ready for thought: So the darkness shall be the light, and the stillness the dancing.', 95 | author: 'T.S. Eliot' 96 | } 97 | ] 98 | 99 | export const random = array => array[Math.floor(Math.random() * array.length)] 100 | export const allowedKeys = [ 101 | 'q', 102 | 'w', 103 | 'e', 104 | 'r', 105 | 't', 106 | 'y', 107 | 'u', 108 | 'i', 109 | 'o', 110 | 'p', 111 | 'a', 112 | 's', 113 | 'd', 114 | 'f', 115 | 'g', 116 | 'h', 117 | 'j', 118 | 'k', 119 | 'l', 120 | 'z', 121 | 'x', 122 | 'c', 123 | 'v', 124 | 'b', 125 | 'n', 126 | 'm', 127 | 'Q', 128 | 'W', 129 | 'E', 130 | 'R', 131 | 'T', 132 | 'Y', 133 | 'U', 134 | 'I', 135 | 'O', 136 | 'P', 137 | 'A', 138 | 'S', 139 | 'D', 140 | 'F', 141 | 'G', 142 | 'H', 143 | 'J', 144 | 'K', 145 | 'L', 146 | 'Z', 147 | 'X', 148 | 'C', 149 | 'V', 150 | 'B', 151 | 'N', 152 | 'M', 153 | ';', 154 | "'", 155 | ',', 156 | '.' 157 | ] 158 | -------------------------------------------------------------------------------- /src/components/ItemList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ItemList = ({ name, data, symble, ...rest }) => { 4 | return ( 5 |
  • 6 | {name} 7 | 8 | {data} 9 | {symble && data > 0 ? {symble} : ''} 10 | 11 |
  • 12 | ) 13 | } 14 | 15 | export default ItemList 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import * as serviceWorker from './serviceWorker' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister() 12 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /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/extend-expect'; 6 | --------------------------------------------------------------------------------