├── src ├── react-app-env.d.ts ├── stylesheets │ ├── Result.scss │ ├── Header.scss │ ├── AnimatedTheme.scss │ ├── CommandPallet.module.scss │ ├── Test.scss │ ├── themes.scss │ └── Footer.scss ├── setupTests.ts ├── store │ ├── store.ts │ ├── actions.ts │ └── reducer.ts ├── helpers │ ├── startTimer.ts │ ├── resetTest.ts │ └── recordTest.ts ├── index.tsx ├── wordlists │ ├── got.json │ ├── hard-words.json │ ├── javascript.json │ ├── python.json │ ├── sentences.json │ ├── numbers.json │ └── words.json ├── index.scss ├── components │ ├── Result.tsx │ ├── Test.tsx │ ├── Footer.tsx │ ├── CommandPallet.tsx │ └── Header.tsx └── App.tsx ├── public ├── favicon.png ├── robots.txt ├── manifest.json └── index.html ├── .husky ├── pre-commit └── commit-msg ├── .prettierrc ├── .commitlintrc ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── node.js.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slmnsh/typing-test/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "useTabs": false, 4 | "tabWidth": 4, 5 | "bracketSameLine": true 6 | } 7 | -------------------------------------------------------------------------------- /src/stylesheets/Result.scss: -------------------------------------------------------------------------------- 1 | table { 2 | font-size: 16pt; 3 | font-weight: 800; 4 | button { 5 | font-size: 21pt; 6 | } 7 | } 8 | 9 | .wrong { 10 | color: var(--hl-color); 11 | } 12 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "type-empty": [2, "never"], 4 | "subject-empty": [2, "never"], 5 | "type-enum": [ 2, "always", [ "build", "chore", "ci", "docs", "improvement", "feat", "fix", "perf", "refactor", "revert", "style", "test" ], ], 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 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/store/store.ts: -------------------------------------------------------------------------------- 1 | import reducer from "./reducer"; 2 | import { createStore } from "redux"; 3 | 4 | export const store = createStore( 5 | reducer, 6 | (window as any).__REDUX_DEVTOOLS_EXTENSION__ && 7 | (window as any).__REDUX_DEVTOOLS_EXTENSION__() 8 | ); 9 | -------------------------------------------------------------------------------- /src/helpers/startTimer.ts: -------------------------------------------------------------------------------- 1 | import { setTimerId, timerDecrement } from "store/actions"; 2 | import { store } from "store/store"; 3 | 4 | export const startTimer = () => { 5 | const { dispatch } = store; 6 | const timerId = setInterval(() => { 7 | dispatch(timerDecrement()); 8 | }, 1000); 9 | dispatch(setTimerId(timerId)); 10 | }; 11 | -------------------------------------------------------------------------------- /.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.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import App from "App"; 5 | import { store } from "./store/store"; 6 | import "index.scss"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | -------------------------------------------------------------------------------- /src/stylesheets/Header.scss: -------------------------------------------------------------------------------- 1 | header, 2 | footer { 3 | margin: 50px 0; 4 | display: flex; 5 | width: 50%; 6 | justify-content: space-between; 7 | align-items: center; 8 | font-size: 12pt; 9 | font-weight: 700; 10 | .buttons { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: flex-end; 14 | } 15 | } 16 | 17 | .brand { 18 | font-size: 21pt; 19 | align-self: center; 20 | } 21 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Typing Test", 3 | "name": "Typing test in React", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "favicon.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 | -------------------------------------------------------------------------------- /src/wordlists/got.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Never forget what you are, The rest of the world will not.", 3 | "Any man who must say, 'I am the king,' is no true king.", 4 | "The things I do for love.", 5 | "There is only one thing we say to death: Not today.", 6 | "If you think this has a happy ending, you haven't been paying attention.", 7 | "You're going to die tomorrow, Lord Bolton. Sleep well.", 8 | "That's what I do: I drink and I know things.", 9 | "The man who passes the sentence should swing the sword.", 10 | "Chaos isn't a pit. Chaos is a ladder.", 11 | "Tell Cersei. I want her to know it wasn't me.", 12 | "You know nothing, Jon Snow.", 13 | "Winter is coming." 14 | ] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/stylesheets/AnimatedTheme.scss: -------------------------------------------------------------------------------- 1 | .animated-theme { 2 | display: block; 3 | position: fixed; 4 | border-radius: 50%; 5 | background-color: var(--bg-color); 6 | transform: translate(-50%, -50%); 7 | overflow: hidden; 8 | z-index: 99; 9 | animation: grow 1s ease-in-out; 10 | &.default { 11 | background-color: black !important; 12 | } 13 | @keyframes grow { 14 | 0% { 15 | min-height: 0; 16 | min-width: 0; 17 | } 18 | 80% { 19 | min-width: 4000px; 20 | min-height: 4000px; 21 | opacity: 1; 22 | } 23 | 100% { 24 | opacity: 0; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/resetTest.ts: -------------------------------------------------------------------------------- 1 | import { setTimerId, setWordList, timerSet } from "store/actions"; 2 | import { store } from "store/store"; 3 | 4 | export const resetTest = async () => { 5 | const { dispatch, getState } = store; 6 | const { 7 | time: { timerId }, 8 | preferences: { timeLimit, type }, 9 | } = getState(); 10 | document 11 | .querySelectorAll(".wrong, .right") 12 | .forEach((el) => el.classList.remove("wrong", "right")); 13 | if (timerId) { 14 | clearInterval(timerId); 15 | dispatch(setTimerId(null)); 16 | } 17 | import(`wordlists/${type}.json`).then((words) => 18 | dispatch(setWordList(words.default)) 19 | ); 20 | dispatch(timerSet(timeLimit)); 21 | }; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Press '....' key 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Linux] 28 | - Browser [e.g. chrome, firefox] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "noImplicitAny": true, 5 | "resolveJsonModule": true, 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "strictBindCallApply": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "allowJs": true, 22 | "baseUrl": "src" 23 | }, 24 | "include": ["src"], 25 | "files.watcherExclude": { 26 | "**/.git/objects/**": true, 27 | "**/.git/subtree-cache/**": true, 28 | "node_modules/**": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Deployment CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - name: GitHub Pages action 30 | uses: peaceiris/actions-gh-pages@v3.7.3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./build 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Salman Shaikh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stylesheets/CommandPallet.module.scss: -------------------------------------------------------------------------------- 1 | .commandPallet { 2 | display: flex; 3 | flex-direction: column; 4 | width: 25vw; 5 | position: fixed; 6 | top: 25%; 7 | left: 50%; 8 | z-index: 999; 9 | transform: translateX(-50%); 10 | background-color: var(--font-color); 11 | color: var(--bg-color); 12 | border: 1px solid var(--hl-color); 13 | box-shadow: 5px 5px var(--hl-color); 14 | } 15 | 16 | .commandPallet:before { 17 | content: ""; 18 | display: block; 19 | height: 105vh; 20 | width: 125vw; 21 | background-color: var(--fg-color); 22 | opacity: 0.6; 23 | z-index: -1; 24 | position: fixed; 25 | top: 0; 26 | left: 0; 27 | transform: translate(-50%, -25%); 28 | pointer-events: none; 29 | } 30 | 31 | .commandInput { 32 | padding: 10px; 33 | background-color: inherit; 34 | color: inherit; 35 | font-family: inherit; 36 | font-size: 18px; 37 | border: none; 38 | outline: none; 39 | } 40 | 41 | .command { 42 | text-transform: capitalize; 43 | font-weight: 700; 44 | padding: 10px; 45 | cursor: pointer; 46 | user-select: none; 47 | } 48 | 49 | .highlighted { 50 | background-color: var(--bg-color); 51 | color: var(--font-color); 52 | } 53 | -------------------------------------------------------------------------------- /src/wordlists/hard-words.json: -------------------------------------------------------------------------------- 1 | [ 2 | "internationalization", 3 | "straightforward", 4 | "duplicate", 5 | "therefore", 6 | "disappointment", 7 | "cultivate", 8 | "translate", 9 | "transform", 10 | "transport", 11 | "pollution", 12 | "watermelon", 13 | "unbelievable", 14 | "incredible", 15 | "millionaire", 16 | "relationship", 17 | "scholarship", 18 | "championship", 19 | "citizenship", 20 | "friendship", 21 | "leadership", 22 | "membership", 23 | "readership", 24 | "childhood", 25 | "adulthood", 26 | "neighborhood", 27 | "likelyhood", 28 | "commitment", 29 | "judgement", 30 | "statement", 31 | "amazement", 32 | "statistics", 33 | "extraodinary", 34 | "universal", 35 | "brainstorm", 36 | "astronomy", 37 | "preoccupied", 38 | "reservation", 39 | "phenomenon", 40 | "irrespective", 41 | "apprehensive", 42 | "circumstance", 43 | "unaccountable", 44 | "uncharacteristically", 45 | "establishmentarianism", 46 | "communication", 47 | "eco-tourism", 48 | "acquisition", 49 | "localization", 50 | "globalization", 51 | "restriction", 52 | "stimulation", 53 | "virtualization", 54 | "technology", 55 | "application", 56 | "sourcecode", 57 | "appearance", 58 | "preference", 59 | "performance", 60 | "exceptionally", 61 | "difficulties", 62 | "dimensionality", 63 | "enhancement", 64 | "documentary", 65 | "originality", 66 | "disability" 67 | ] 68 | -------------------------------------------------------------------------------- /src/wordlists/javascript.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Array", 3 | "Date", 4 | "eval", 5 | "function", 6 | "hasOwnProperty", 7 | "Infinity", 8 | "isFinite", 9 | "isNaN", 10 | "isPrototypeOf", 11 | "length", 12 | "Math", 13 | "NaN", 14 | "name", 15 | "Number", 16 | "Object", 17 | "prototype", 18 | "String", 19 | "toString", 20 | "undefined", 21 | "valueOf", 22 | "abstract", 23 | "arguments", 24 | "await*", 25 | "boolean", 26 | "break", 27 | "byte", 28 | "case", 29 | "catch", 30 | "char", 31 | "class*", 32 | "const", 33 | "continue", 34 | "debugger", 35 | "default", 36 | "delete", 37 | "do", 38 | "double", 39 | "else", 40 | "enum*", 41 | "eval", 42 | "export*", 43 | "extends*", 44 | "false", 45 | "final", 46 | "finally", 47 | "float", 48 | "for", 49 | "function", 50 | "goto", 51 | "if", 52 | "implements", 53 | "import*", 54 | "in", 55 | "instanceof", 56 | "int", 57 | "interface", 58 | "let*", 59 | "long", 60 | "native", 61 | "new", 62 | "null", 63 | "package", 64 | "private", 65 | "protected", 66 | "public", 67 | "return", 68 | "short", 69 | "static", 70 | "super*", 71 | "switch", 72 | "synchronized", 73 | "this", 74 | "throw", 75 | "throws", 76 | "transient", 77 | "true", 78 | "try", 79 | "typeof", 80 | "var", 81 | "void", 82 | "volatile", 83 | "while", 84 | "with", 85 | "yield" 86 | ] 87 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap"); 2 | 3 | body { 4 | margin: 0; 5 | font-family: Inconsolata, monospace; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | html, 11 | body { 12 | height: 100%; 13 | width: 100%; 14 | scroll-behavior: smooth; 15 | } 16 | 17 | #root { 18 | display: flex; 19 | height: 100%; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: space-between; 23 | color: var(--font-color); 24 | background-color: var(--bg-color); 25 | } 26 | 27 | button { 28 | background-color: var(--bg-color); 29 | color: var(--font-color); 30 | border: 2px solid var(--font-color); 31 | padding: 10px; 32 | font-family: Inconsolata, monospace; 33 | font-weight: 700; 34 | margin: 10px; 35 | cursor: pointer; 36 | &.mini { 37 | font-size: 12pt; 38 | border: none; 39 | padding: 5px; 40 | margin: 0; 41 | &.selected { 42 | color: var(--hl-color); 43 | } 44 | } 45 | &:hover { 46 | color: var(--hl-color); 47 | border-color: var(--hl-color); 48 | } 49 | } 50 | 51 | a { 52 | color: var(--font-color); 53 | text-decoration: none; 54 | transition: color 0.1s ease; 55 | span { 56 | font-size: smaller; 57 | margin: 2.5px; 58 | } 59 | &:hover { 60 | color: var(--hl-color); 61 | } 62 | } 63 | 64 | .hidden { 65 | opacity: 0; 66 | pointer-events: none; 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Result.tsx: -------------------------------------------------------------------------------- 1 | import { resetTest } from "helpers/resetTest"; 2 | import { useSelector } from "react-redux"; 3 | import { State } from "store/reducer"; 4 | import "stylesheets/Result.scss"; 5 | 6 | export default function Result() { 7 | const { 8 | word: { wordList, typedHistory, currWord }, 9 | preferences: { timeLimit }, 10 | } = useSelector((state: State) => state); 11 | const spaces = wordList.indexOf(currWord); 12 | let correctChars = 0; 13 | const result = typedHistory.map( 14 | (typedWord, idx) => typedWord === wordList[idx] 15 | ); 16 | result.forEach((r, idx) => { 17 | if (r) correctChars += wordList[idx].length; 18 | }); 19 | const wpm = ((correctChars + spaces) * 60) / timeLimit / 5; 20 | return ( 21 |
22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 |
26 |

{Math.round(wpm) + " wpm"}

27 |
Correct Words:{result.filter((x) => x).length}
Incorrect Words:{result.filter((x) => !x).length}
39 | 40 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typing-test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.1", 8 | "@testing-library/react": "^12.1.2", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.4.0", 11 | "@types/node": "^17.0.10", 12 | "@types/react": "^17.0.38", 13 | "@types/react-dom": "^17.0.11", 14 | "@types/react-redux": "^7.1.22", 15 | "eslint-config-react-app": "^7.0.0", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2", 18 | "react-redux": "^7.2.6", 19 | "react-scripts": "^5.0.0", 20 | "redux": "^4.1.2", 21 | "sass": "^1.49.0", 22 | "sass-loader": "^12.4.0", 23 | "typescript": "^4.5.4", 24 | "web-vitals": "^2.1.3" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject", 31 | "prepare": "husky install" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@commitlint/cli": "^17.1.2", 53 | "@commitlint/config-conventional": "^17.1.0", 54 | "husky": "^8.0.0", 55 | "prettier": "^2.7.1", 56 | "pretty-quick": "^3.1.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/stylesheets/Test.scss: -------------------------------------------------------------------------------- 1 | .test { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | font-weight: 700; 6 | height: 99.8%; 7 | width: 50%; 8 | } 9 | .timer { 10 | font-size: 21pt; 11 | margin: 5px; 12 | color: var(--hl-color); 13 | } 14 | .box { 15 | font-size: 21pt; 16 | overflow: hidden; 17 | height: 93px; 18 | display: flex; 19 | flex-wrap: wrap; 20 | align-items: center; 21 | user-select: none; 22 | .word { 23 | position: relative; 24 | margin: 0 5px 2px; 25 | &.wrong { 26 | text-decoration: 2px underline var(--hl-color); 27 | animation: shake 0.1s ease; 28 | @keyframes shake { 29 | 0% { 30 | transform: translateX(5px); 31 | } 32 | 50% { 33 | transform: translateX(-5px); 34 | } 35 | 100% { 36 | transform: translateX(0); 37 | } 38 | } 39 | } 40 | } 41 | .typedWord { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | } 46 | .right { 47 | color: var(--fg-color); 48 | } 49 | #active { 50 | position: relative; 51 | width: max-content; 52 | } 53 | span.wrong { 54 | color: var(--hl-color) !important; 55 | } 56 | .extra { 57 | opacity: 0.6; 58 | } 59 | 60 | #caret { 61 | transition: left 0.1s ease; 62 | margin-left: -7.29165px; 63 | position: absolute; 64 | color: var(--hl-color); 65 | &.blink { 66 | animation: blink 1.5s infinite 1s; 67 | } 68 | } 69 | @keyframes blink { 70 | 0%, 71 | 100% { 72 | opacity: 1; 73 | } 74 | 50% { 75 | opacity: 0; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/wordlists/python.json: -------------------------------------------------------------------------------- 1 | [ 2 | "False", 3 | "None", 4 | "True", 5 | "and", 6 | "as", 7 | "assert", 8 | "async", 9 | "await", 10 | "break", 11 | "class", 12 | "continue", 13 | "def", 14 | "del", 15 | "elif", 16 | "else", 17 | "except", 18 | "finally", 19 | "for", 20 | "from", 21 | "global", 22 | "if", 23 | "import", 24 | "in", 25 | "is", 26 | "lambda", 27 | "nonlocal", 28 | "not", 29 | "or", 30 | "pass", 31 | "raise", 32 | "return", 33 | "try", 34 | "while", 35 | "with", 36 | "yield", 37 | "abs", 38 | "aiter", 39 | "all", 40 | "any", 41 | "anext", 42 | "ascii", 43 | "bin", 44 | "bool", 45 | "breakpoint", 46 | "bytearray", 47 | "bytes", 48 | "callable", 49 | "chr", 50 | "classmethod", 51 | "compile", 52 | "complex", 53 | "delattr", 54 | "dict", 55 | "dir", 56 | "divmod", 57 | "enumerate", 58 | "eval", 59 | "exec", 60 | "filter", 61 | "float", 62 | "format", 63 | "frozenset", 64 | "getattr", 65 | "globals", 66 | "hasattr", 67 | "hash", 68 | "help", 69 | "hex", 70 | "id", 71 | "input", 72 | "int", 73 | "isinstance", 74 | "issubclass", 75 | "iter", 76 | "len", 77 | "list", 78 | "locals", 79 | "map", 80 | "max", 81 | "memoryview", 82 | "min", 83 | "next", 84 | "object", 85 | "oct", 86 | "open", 87 | "ord", 88 | "pow", 89 | "print", 90 | "property", 91 | "range", 92 | "repr", 93 | "reversed", 94 | "round", 95 | "set", 96 | "setattr", 97 | "slice", 98 | "sorted", 99 | "staticmethod", 100 | "str", 101 | "sum", 102 | "super", 103 | "tuple", 104 | "type", 105 | "vars", 106 | "zip" 107 | ] 108 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 16 | 17 | 26 | Typing Test 27 | 28 | 29 | 30 |
31 | 41 | 42 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | 3 | export const SET_WORD = "SETWORD"; 4 | export const SET_CHAR = "SETCHAR"; 5 | export const TIMER_DECREMENT = "TIMERDECREMENT"; 6 | export const APPEND_TYPED_HISTORY = "APPENDTYPEDHISTORY"; 7 | export const TIMER_SET = "TIMERSET"; 8 | export const TIMERID_SET = "TIMERIDSET"; 9 | export const PREV_WORD = "PREVWORD"; 10 | export const SET_WORDLIST = "SETWORDLIST"; 11 | export const SET_THEME = "SETTHEME"; 12 | export const SET_TIME = "SETTIME"; 13 | export const SET_REF = "SETREF"; 14 | export const SET_CARET_REF = "SETCARETREF"; 15 | export const SET_TYPE = "SETTYPE"; 16 | 17 | // Time Actions 18 | export const timerDecrement = () => ({ type: TIMER_DECREMENT }); 19 | export const timerSet = (payload: number) => ({ type: TIMER_SET, payload }); 20 | export const setTimerId = (payload: NodeJS.Timer | null) => ({ 21 | type: TIMERID_SET, 22 | payload, 23 | }); 24 | 25 | // Word Actions 26 | export const setWord = (payload: string) => ({ type: SET_WORD, payload }); 27 | export const setChar = (payload: string) => ({ type: SET_CHAR, payload }); 28 | export const setTypedWord = (payload: string) => ({ type: SET_CHAR, payload }); 29 | export const appendTypedHistory = () => ({ 30 | type: APPEND_TYPED_HISTORY, 31 | }); 32 | export const backtrackWord = (payload: boolean) => ({ 33 | type: PREV_WORD, 34 | payload, 35 | }); 36 | export const setWordList = (payload: string[]) => ({ 37 | type: SET_WORDLIST, 38 | payload, 39 | }); 40 | export const setRef = (payload: RefObject) => ({ 41 | type: SET_REF, 42 | payload, 43 | }); 44 | export const setCaretRef = (payload: RefObject) => ({ 45 | type: SET_CARET_REF, 46 | payload, 47 | }); 48 | 49 | // Prefrences Actions 50 | export const setTheme = (payload: string) => ({ type: SET_THEME, payload }); 51 | export const setTime = (payload: number) => ({ type: SET_TIME, payload }); 52 | export const setType = (payload: string) => ({ 53 | type: SET_TYPE, 54 | payload, 55 | }); 56 | -------------------------------------------------------------------------------- /src/wordlists/sentences.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Sarah and Ira drove to the store.", 3 | "The ham, green beans, mashed potatoes, and corn are gluten-free.", 4 | "My mother hemmed and hawed over where to go for dinner.", 5 | "The mangy, scrawny stray dog hurriedly gobbled down the grain-free, organic dog food.", 6 | "I quickly put on my red winter jacket, black snow pants, waterproof boots, homemade mittens, and handknit scarf.", 7 | "The incessant ticking and chiming echoed off the weathered walls of the clock repair shop.", 8 | "Nervously, I unfolded the wrinkled and stained letter from my long-dead ancestor.", 9 | "Into the suitcase, I carelessly threw a pair of ripped jeans, my favorite sweater from high school, an old pair of tube socks with stripes, and $20,000 in cash.", 10 | "The cat and dog ate.", 11 | "My parents and I went to a movie", 12 | "Mrs. Juarez and Mr. Smith are dancing gracefully.", 13 | "Samantha, Elizabeth, and Joan are on the committee.", 14 | "The paper and pencil sat idle on the desk.", 15 | "I rinsed and dried the dishes.", 16 | "The Spirits of All Three shall strive within me.", 17 | "The sun is shining brightly.", 18 | "Birds are chirping outside my window.", 19 | "I enjoy reading books.", 20 | "Cooking is a fun activity.", 21 | "Your life is too important to let it control you.", 22 | "As I stepped out of the car and onto the sand, it all melted.", 23 | "Moved by his kindness, she could not utter a single word, tears blurring her eyes.", 24 | "Fear slowly crept upon her and her legs shook involuntarily.", 25 | "The grief had a gravity, pulling me down, but a tiny voice whispered in my mind.", 26 | "The news cast a cloud of gloom over his face and he couldn't help crying bitterly.", 27 | "She sobbed, hiding her face in her hands.", 28 | "She laughed, eyes sparkling excitement.", 29 | "Staring at that beast, she gathered all her courage and shouted at it.", 30 | "Amy begged in an eager voice, clinging on to her mom's sleeve.", 31 | "He stormed out of the room in a burst of anger and slammed the door furiously.", 32 | "My legs tingled and shook ,but I staggered away.", 33 | "Without any hesitation, he immediately rushed out of the house, got into his car and drove away." 34 | ] 35 | -------------------------------------------------------------------------------- /src/stylesheets/themes.scss: -------------------------------------------------------------------------------- 1 | .default { 2 | --bg-color: black; 3 | --font-color: lightslategray; 4 | --hl-color: orange; 5 | --fg-color: #292f34; 6 | } 7 | 8 | .mkbhd { 9 | --bg-color: #171717; 10 | --font-color: #ededed; 11 | --hl-color: #da0037; 12 | --fg-color: #444444; 13 | } 14 | 15 | .mocha { 16 | --bg-color: #2d2424; 17 | --font-color: #e0c097; 18 | --hl-color: #dc521c; 19 | --fg-color: #5c3d2e; 20 | } 21 | 22 | .coral { 23 | --bg-color: #fdd2bf; 24 | --font-color: #df5e5e; 25 | --hl-color: #492f10; 26 | --fg-color: #e98580; 27 | } 28 | 29 | .ocean { 30 | --bg-color: #dddddd; 31 | --font-color: #125d98; 32 | --hl-color: #f5a962; 33 | --fg-color: #3c8dad; 34 | } 35 | 36 | .azure { 37 | --bg-color: #383e56; 38 | --font-color: #eedad1; 39 | --hl-color: #f69e7b; 40 | --fg-color: #d4b5b0; 41 | } 42 | 43 | .forest { 44 | --bg-color: #334443; 45 | --font-color: #fff8df; 46 | --hl-color: #c6ffc1; 47 | --fg-color: #34656d; 48 | } 49 | 50 | .rose-milk { 51 | --bg-color: #ffffff; 52 | --font-color: #111111; 53 | --hl-color: #b33838; 54 | --fg-color: #3b8792; 55 | } 56 | 57 | .amethyst { 58 | --bg-color: #e2caea; 59 | --font-color: #480c51; 60 | --hl-color: #ab395f; 61 | --fg-color: #9774aa; 62 | } 63 | 64 | .amber { 65 | --bg-color: #feb204; 66 | --font-color: #d53600; 67 | --hl-color: #700e01; 68 | --fg-color: #ff8503; 69 | } 70 | 71 | .terminal { 72 | --bg-color: #000000; 73 | --font-color: #ffffff; 74 | --hl-color: #ff0000; 75 | --fg-color: #00ff00; 76 | } 77 | 78 | .vscode { 79 | --bg-color: #1e1e1e; 80 | --font-color: #d4d4d4; 81 | --hl-color: #ce9178; 82 | --fg-color: #474747; 83 | } 84 | 85 | .mountain { 86 | --bg-color: #10271e; 87 | --font-color: #d1ac3f; 88 | --hl-color: #a2d6f6; 89 | --fg-color: #485e2c; 90 | } 91 | 92 | 93 | .red-season { 94 | --bg-color:#1e1c1c; 95 | --font-color:#e73535; 96 | --hl-color: #d4d4d4; 97 | --fg-color: #593b3e; 98 | } 99 | 100 | .pink-sky { 101 | --bg-color: #23049D; 102 | --font-color: #FF79CD; 103 | --hl-color: #FFDF6B; 104 | --fg-color: #AA2EE6; 105 | } 106 | -------------------------------------------------------------------------------- /src/stylesheets/Footer.scss: -------------------------------------------------------------------------------- 1 | .bottom-area { 2 | width: 50%; 3 | justify-content: center; 4 | position: relative; 5 | .hint { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | kbd { 10 | background-color: var(--font-color); 11 | color: var(--bg-color); 12 | padding: 2.5px 5px; 13 | margin: 10px; 14 | border-radius: 3px; 15 | font-size: 9pt; 16 | } 17 | } 18 | footer { 19 | width: 100%; 20 | button, 21 | & > a { 22 | font-size: 12pt; 23 | border: none; 24 | width: 150px; 25 | margin: 0; 26 | padding: 0 10px; 27 | } 28 | .contributor-list { 29 | display: flex; 30 | flex-direction: column; 31 | position: absolute; 32 | bottom: 90px; 33 | right: 10px; 34 | max-height: 300%; 35 | width: 300px; 36 | overflow-y: auto; 37 | background-color: var(--bg-color); 38 | border: 1px solid var(--fg-color); 39 | box-shadow: 5px 5px var(--fg-color); 40 | scrollbar-width: thin; 41 | h2 { 42 | position: sticky; 43 | top: 0; 44 | background-color: var(--bg-color); 45 | padding: 15px; 46 | margin: 0; 47 | } 48 | .contributor { 49 | display: flex; 50 | flex-direction: row; 51 | padding: 10px; 52 | img { 53 | border-radius: 50%; 54 | margin: 5px; 55 | } 56 | .contributor-details { 57 | display: flex; 58 | flex-direction: column; 59 | justify-content: space-around; 60 | padding: 5px 15px; 61 | } 62 | } 63 | &::-webkit-scrollbar { 64 | width: 5px; 65 | } 66 | &::-webkit-scrollbar-thumb { 67 | background-color: var(--font-color); 68 | border-radius: 10px; 69 | } 70 | &::-webkit-scrollbar-track-piece { 71 | background-color: var(--bg-color); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typing-test 2 | 3 | ![Deployment CI](https://github.com/salmannotkhan/typing-test/actions/workflows/node.js.yml/badge.svg) 4 | 5 | ![typing-test(test)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dv44pfwm7qsud43xheei.png) 6 | 7 | NOTE: This is my recreation of already existing [monkeytype](https://monkeytype.com) 8 | 9 | This site is currently live: [Visit Here](https://slmn.sh/typing-test/) 10 | 11 | ## How to run locally 12 | 13 | ```zsh 14 | git clone https://github.com/salmannotkhan/typing-test.git 15 | cd typing-test 16 | npm install 17 | npm start # to start local server at `localhost:3000` 18 | npm run build # to create production build run 19 | ``` 20 | 21 | ## Got new ideas? 22 | 23 | Did you know? You can add your theme and wordlist ideas into typing-test. 24 | 25 | Here is how you can do it: 26 | 27 | ### **To add new theme:** 28 | 29 | - Add theme colors into `src/stylesheets/themes.scss` in following format: 30 | 31 | ```scss 32 | .theme-name { 33 | --bg-color: background-color; 34 | --font-color: font-color; 35 | --hl-color: highlight-color; 36 | --fg-color: forground-color; 37 | } 38 | ``` 39 | 40 | > **Note:** 41 | > `highlight-color` is used for caret, wrong characters, timer, selected and onhover colors 42 | > `forground-color` is used for correctly typed characters 43 | > _Using hex codes for colors is recommended_ 44 | 45 | ### **To add new wordlist:** 46 | 47 | - Rename your wordlist as `.json` and place it inside `src/wordlists`. 48 | 49 | > **Important:** 50 | > The JSON file should only contain single array of words/sentences. 51 | 52 | ### **Adding entry to options** 53 | 54 | 1. Add your theme/wordlist name into `src/components/Header.tsx` in options: 55 | 56 | ```tsx 57 | export const options: Options = { 58 | time: [15, 30, 45, 60, 120], 59 | theme: [ 60 | "default", 61 | "mkbhd", 62 | "mocha", 63 | "coral", 64 | "ocean", 65 | "azure", 66 | "forest", 67 | "rose-milk", 68 | 69 | ], 70 | type: ["words", "sentences", ], 71 | }; 72 | ``` 73 | 74 | > **Important:** 75 | > The following should be always same: 76 | > 77 | > - wordlist-name in `Header.tsx` and your wordlist file name 78 | > - theme-name in `themes.scss` and `Header.tsx` 79 | > 80 | > should always match otherwise themes won't work 81 | 82 | 2. Make a pull request 83 | 84 | 3. If it's good enough to merge, I'll merge it 85 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import Header from "components/Header"; 4 | import Test from "components/Test"; 5 | import Result from "components/Result"; 6 | import Footer from "components/Footer"; 7 | import { State } from "store/reducer"; 8 | import { setTimerId } from "store/actions"; 9 | import { recordTest } from "helpers/recordTest"; 10 | import "stylesheets/themes.scss"; 11 | import CommandPallet from "components/CommandPallet"; 12 | 13 | export default function App() { 14 | const { 15 | time: { timerId, timer }, 16 | word: { currWord, typedWord, activeWordRef }, 17 | } = useSelector((state: State) => state); 18 | const dispatch = useDispatch(); 19 | const [showPallet, setShowPallet] = useState(false); 20 | 21 | useEffect(() => { 22 | document.onkeydown = (e) => { 23 | if (e.ctrlKey && e.key === "k") { 24 | setShowPallet((s) => !s); 25 | e.preventDefault(); 26 | } else if ( 27 | e.key.length === 1 || 28 | e.key === "Backspace" || 29 | e.key === "Tab" 30 | ) { 31 | recordTest(e.key, e.ctrlKey); 32 | e.preventDefault(); 33 | } 34 | }; 35 | return () => { 36 | document.onkeydown = null; 37 | }; 38 | }, [dispatch]); 39 | 40 | useEffect(() => { 41 | let idx = typedWord.length - 1; 42 | const currWordEl = activeWordRef?.current!; 43 | if (currWordEl) { 44 | currWordEl.children[idx + 1].classList.add( 45 | currWord[idx] !== typedWord[idx] ? "wrong" : "right" 46 | ); 47 | } 48 | }, [currWord, typedWord, activeWordRef]); 49 | 50 | useEffect(() => { 51 | let idx = typedWord.length; 52 | const currWordEl = activeWordRef?.current!; 53 | if (currWordEl && idx < currWord.length) 54 | currWordEl.children[idx + 1].classList.remove("wrong", "right"); 55 | }, [currWord.length, typedWord, activeWordRef]); 56 | 57 | useEffect(() => { 58 | if (!timer && timerId) { 59 | clearInterval(timerId); 60 | dispatch(setTimerId(null)); 61 | } 62 | }, [dispatch, timer, timerId]); 63 | 64 | return ( 65 | <> 66 |
67 | {showPallet && } 68 | {timer ? : } 69 |