├── .gitignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── assets
│ ├── Flash-PNG-Pic.png
│ ├── flash.png
│ └── logo.png
├── components
│ ├── App
│ │ ├── App.css
│ │ └── App.jsx
│ ├── ChallengeDetailsCard
│ │ ├── ChallengeDetailsCard.css
│ │ └── ChallengeDetailsCard.jsx
│ ├── ChallengeSection
│ │ ├── ChallengeSection.css
│ │ └── ChallengeSection.jsx
│ ├── Footer
│ │ ├── Footer.css
│ │ └── Footer.jsx
│ ├── Landing
│ │ ├── Landing.css
│ │ └── Landing.jsx
│ ├── Nav
│ │ ├── Nav.css
│ │ └── Nav.jsx
│ ├── TestContainer
│ │ ├── TestContainer.css
│ │ └── TestContainer.jsx
│ ├── TestLetter
│ │ ├── TestLetter.css
│ │ └── TestLetter.jsx
│ ├── TryAgain
│ │ ├── TryAgain.css
│ │ └── TryAgain.jsx
│ ├── TypingChallenge
│ │ ├── TypingChallenge.css
│ │ └── TypingChallenge.jsx
│ └── TypingChallengeContainer
│ │ ├── TypingChallengeContainer.css
│ │ └── TypingChallengeContainer.jsx
├── data
│ └── sampleParagraphs.js
└── index.js
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flash Type
2 |
3 | A simple typing speed test application
4 |
5 | ## Libraries used
6 |
7 | https://www.npmjs.com/package/typewriter-effect
8 |
9 | https://michalsnik.github.io/aos/
10 |
11 | ## Include the fonts and aos in index.html
12 |
13 | **Inside
**
14 |
15 | ```
16 |
17 |
18 |
19 |
20 | ```
21 |
22 | **Inside **
23 |
24 | ```html
25 |
26 |
29 | ```
30 |
31 | ## API used
32 |
33 | http://metaphorpsum.com/paragraphs/1/8
34 |
35 | ## Credits
36 |
37 | 1. Flash Image taken from - http://www.pngmart.com/image/tag/flash
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flashtype",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "http://theleanprogrammer.com/flashtype/",
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.11.4",
8 | "@testing-library/react": "^11.1.0",
9 | "@testing-library/user-event": "^12.1.10",
10 | "gh-pages": "^3.1.0",
11 | "react": "^17.0.1",
12 | "react-dom": "^17.0.1",
13 | "react-scripts": "4.0.2",
14 | "typewriter-effect": "^2.17.0",
15 | "web-vitals": "^1.0.1"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "eject": "react-scripts eject",
22 | "predeploy": "npm run build",
23 | "deploy": "gh-pages -d build"
24 | },
25 | "eslintConfig": {
26 | "extends": [
27 | "react-app",
28 | "react-app/jest"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLeanProgrammer/flashtype/eea9461246be0f9de183fd10474279d55e8597a1/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
24 |
33 | FlashType
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
48 |
49 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLeanProgrammer/flashtype/eea9461246be0f9de183fd10474279d55e8597a1/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLeanProgrammer/flashtype/eea9461246be0f9de183fd10474279d55e8597a1/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 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/assets/Flash-PNG-Pic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLeanProgrammer/flashtype/eea9461246be0f9de183fd10474279d55e8597a1/src/assets/Flash-PNG-Pic.png
--------------------------------------------------------------------------------
/src/assets/flash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLeanProgrammer/flashtype/eea9461246be0f9de183fd10474279d55e8597a1/src/assets/flash.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLeanProgrammer/flashtype/eea9461246be0f9de183fd10474279d55e8597a1/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/App/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | box-sizing: border-box;
4 | font-family: "Poppins", sans-serif;
5 | }
6 |
7 | .app {
8 | display: flex;
9 | flex-direction: column;
10 | background: #262a2b;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/App/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { SAMPLE_PARAGRAPHS } from "../../data/sampleParagraphs";
3 | import ChallengeSection from "../ChallengeSection/ChallengeSection";
4 | import Footer from "../Footer/Footer";
5 | import Landing from "../Landing/Landing";
6 | import Nav from "../Nav/Nav";
7 | import "./App.css";
8 |
9 | /**
10 | * Schema of Test Info:
11 | * [
12 | * {
13 | * testLetter: 'H',
14 | * status: correct/incorrect/notAttempted
15 | * }, {
16 | * testLetter: 'e',
17 | * status: correct/incorrect/notAttempted
18 | * }
19 | * ]
20 | */
21 |
22 | const TotalTime = 60;
23 | const DefaultState = {
24 | selectedParagraph: "Hello World!",
25 | testInfo: [],
26 | timerStarted: false,
27 | timeRemaining: TotalTime,
28 | words: 0,
29 | characters: 0,
30 | wpm: 0,
31 | };
32 |
33 | class App extends React.Component {
34 | // state = {
35 | // selectedParagraph: "Hello World!",
36 | // testInfo: [],
37 | // timerStarted: false,
38 | // timeRemaining: TotalTime,
39 | // words: 0,
40 | // characters: 0,
41 | // wpm: 0,
42 | // };
43 | state = DefaultState;
44 |
45 | fetchNewParagraphFallback = () => {
46 | const data =
47 | SAMPLE_PARAGRAPHS[
48 | Math.floor(Math.random() * SAMPLE_PARAGRAPHS.length)
49 | ];
50 |
51 | const selectedParagraphArray = data.split("");
52 | const testInfo = selectedParagraphArray.map((selectedLetter) => {
53 | return {
54 | testLetter: selectedLetter,
55 | status: "notAttempted",
56 | };
57 | });
58 |
59 | // Update the testInfo in state
60 | this.setState({
61 | ...DefaultState,
62 | selectedParagraph: data,
63 | testInfo,
64 | });
65 | };
66 |
67 | fetchNewParagraph = () => {
68 | fetch("http://metaphorpsum.com/paragraphs/1/9")
69 | .then((response) => response.text())
70 | .then((data) => {
71 | // Once the api results are here, break the selectedParagraph into test info
72 | const selectedParagraphArray = data.split("");
73 | const testInfo = selectedParagraphArray.map(
74 | (selectedLetter) => {
75 | return {
76 | testLetter: selectedLetter,
77 | status: "notAttempted",
78 | };
79 | }
80 | );
81 |
82 | // Update the testInfo in state
83 | this.setState({
84 | ...DefaultState,
85 | selectedParagraph: data,
86 | testInfo,
87 | });
88 | });
89 | };
90 |
91 | componentDidMount() {
92 | // As soon as the component mounts, load the selected paragraph from the API
93 | this.fetchNewParagraphFallback();
94 | }
95 |
96 | startAgain = () => this.fetchNewParagraphFallback();
97 |
98 | startTimer = () => {
99 | this.setState({ timerStarted: true });
100 | const timer = setInterval(() => {
101 | if (this.state.timeRemaining > 0) {
102 | // Change the WPM and Time Remaining
103 | const timeSpent = TotalTime - this.state.timeRemaining;
104 | const wpm =
105 | timeSpent > 0
106 | ? (this.state.words / timeSpent) * TotalTime
107 | : 0;
108 | this.setState({
109 | timeRemaining: this.state.timeRemaining - 1,
110 | wpm: parseInt(wpm),
111 | });
112 | } else {
113 | clearInterval(timer);
114 | }
115 | }, 1000);
116 | };
117 |
118 | handleUserInput = (inputValue) => {
119 | if (!this.state.timerStarted) this.startTimer();
120 |
121 | /**
122 | * 1. Handle the underflow case - all characters should be shown as not-attempted
123 | * 2. Handle the overflow case - early exit
124 | * 3. Handle the backspace case
125 | * - Mark the [index+1] element as notAttempted
126 | * (irrespective of whether the index is less than zero)
127 | * - But, don't forget to check for the overflow here
128 | * (index + 1 -> out of bound, when index === length-1)
129 | * 4. Update the status in test info
130 | * 1. Find out the last character in the inputValue and it's index
131 | * 2. Check if the character at same index in testInfo (state) matches
132 | * 3. Yes -> Correct
133 | * No -> Incorrect (Mistake++)
134 | * 5. Irrespective of the case, characters, words and wpm can be updated
135 | */
136 |
137 | const characters = inputValue.length;
138 | const words = inputValue.split(" ").length;
139 | const index = characters - 1;
140 |
141 | if (index < 0) {
142 | this.setState({
143 | testInfo: [
144 | {
145 | testLetter: this.state.testInfo[0].testLetter,
146 | status: "notAttempted",
147 | },
148 | ...this.state.testInfo.slice(1),
149 | ],
150 | characters,
151 | words,
152 | });
153 |
154 | return;
155 | }
156 |
157 | if (index >= this.state.selectedParagraph.length) {
158 | this.setState({
159 | characters,
160 | words,
161 | });
162 | return;
163 | }
164 |
165 | // Make a copy
166 | const testInfo = this.state.testInfo;
167 | if (!(index === this.state.selectedParagraph.length - 1))
168 | testInfo[index + 1].status = "notAttempted";
169 |
170 | // Check for mistake
171 | const isMistake = inputValue[index] === testInfo[index].testLetter;
172 |
173 | // Update the testInfo
174 | testInfo[index].status = isMistake ? "correct" : "incorrect";
175 |
176 | // Update the state
177 | this.setState({
178 | testInfo,
179 | words,
180 | characters,
181 | });
182 | };
183 |
184 | render() {
185 | return (
186 |
187 |
188 |
189 |
200 |
201 |
202 | );
203 | }
204 | }
205 |
206 | export default App;
207 |
--------------------------------------------------------------------------------
/src/components/ChallengeDetailsCard/ChallengeDetailsCard.css:
--------------------------------------------------------------------------------
1 | .details-card-container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | flex-grow: 1;
7 | padding: 10px;
8 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
9 | transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
10 | }
11 |
12 | .details-card-container > p {
13 | margin: 0;
14 | }
15 |
16 | .details-card-container:hover {
17 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
18 | }
19 |
20 | .card-name {
21 | font-size: 15px;
22 | /* padding-top: 16px; */
23 | margin-bottom: -15px !important;
24 | }
25 |
26 | .card-value {
27 | font-size: 50px;
28 | font-weight: 800;
29 | margin: 0;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ChallengeDetailsCard/ChallengeDetailsCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./ChallengeDetailsCard.css";
3 |
4 | const ChallengeDetailsCard = ({ cardName, cardValue }) => {
5 | return (
6 |
7 |
{cardName}
8 |
{cardValue}
9 |
10 | );
11 | };
12 |
13 | export default ChallengeDetailsCard;
14 |
--------------------------------------------------------------------------------
/src/components/ChallengeSection/ChallengeSection.css:
--------------------------------------------------------------------------------
1 | .challenge-section-container {
2 | min-height: 90vh;
3 | flex-direction: column;
4 | align-items: center;
5 | margin: 24px 0;
6 | }
7 |
8 | .challenge-section-header {
9 | text-align: center;
10 | color: #fbf6fb;
11 | font-family: "Bangers", cursive;
12 | font-size: 64px;
13 | letter-spacing: 4px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ChallengeSection/ChallengeSection.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TestContainer from "../TestContainer/TestContainer";
3 | import "./ChallengeSection.css";
4 |
5 | const ChallengeSection = ({
6 | selectedParagraph,
7 | testInfo,
8 | onInputChange,
9 | words,
10 | characters,
11 | wpm,
12 | timeRemaining,
13 | timerStarted,
14 | startAgain,
15 | }) => {
16 | return (
17 |
18 |
19 | Take a Speed Test Now!
20 |
21 |
32 |
33 | );
34 | };
35 |
36 | export default ChallengeSection;
37 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.css:
--------------------------------------------------------------------------------
1 | .footer-container {
2 | display: flex;
3 | justify-content: center;
4 | background: #1d2020;
5 | margin: 0;
6 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
7 | z-index: 99;
8 | }
9 |
10 | .footer-link {
11 | font-family: "Poppins", sans-serif;
12 | font-size: 26px;
13 | /* color: #34353b; */
14 | color: #e7e6e7;
15 | margin: 10px;
16 | text-align: center;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Footer.css";
3 |
4 | const Footer = () => {
5 | return (
6 |
16 | );
17 | };
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/src/components/Landing/Landing.css:
--------------------------------------------------------------------------------
1 | .landing-container {
2 | display: flex;
3 | min-height: 90vh;
4 | flex-direction: row;
5 | justify-content: space-between;
6 | align-items: center;
7 | color: #eee2ee;
8 | padding: 0 32px;
9 | font-family: "Bangers", cursive;
10 | margin: 0;
11 | flex-wrap: wrap;
12 | }
13 |
14 | .landing-header {
15 | font-size: 92px;
16 | margin: 0;
17 | letter-spacing: 4px;
18 | }
19 |
20 | .typewriter-container {
21 | font-size: 64px;
22 | letter-spacing: 2px;
23 | }
24 |
25 | .flash-image {
26 | width: 40vw;
27 | }
28 |
29 | @media (max-width: 800px) {
30 | .landing-container {
31 | flex-direction: column;
32 | align-items: center;
33 | }
34 |
35 | .flash-image {
36 | width: 80vw;
37 | }
38 |
39 | .landing-header {
40 | margin-top: 32px;
41 | text-align: center;
42 | }
43 |
44 | .typewriter-container {
45 | text-align: center;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Landing/Landing.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Typewriter from "typewriter-effect";
3 |
4 | import "./Landing.css";
5 | import flash from "./../../assets/flash.png";
6 |
7 | const Landing = () => {
8 | return (
9 |
10 |
11 |
Can you type
12 |
13 |
20 |
21 |
22 |
23 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Landing;
35 |
--------------------------------------------------------------------------------
/src/components/Nav/Nav.css:
--------------------------------------------------------------------------------
1 | .nav-container {
2 | display: flex;
3 | height: 72px;
4 | align-items: center;
5 | background: #1d2020;
6 | margin: 0;
7 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
8 | justify-content: space-between;
9 | z-index: 99;
10 | }
11 |
12 | .nav-left {
13 | display: flex;
14 | align-items: center;
15 | }
16 |
17 | .nav-right {
18 | margin-right: 20px;
19 | }
20 |
21 | .flash-logo {
22 | margin-left: 16px;
23 | height: 40px;
24 | }
25 |
26 | .flash-logo-text {
27 | font-family: "Bangers", cursive;
28 | font-size: 40px;
29 | letter-spacing: 1px;
30 | margin: 0 10px;
31 | /* color: #34353b; */
32 | color: #e7e6e7;
33 | }
34 |
35 | .nav-aam-link {
36 | font-family: "Poppins", sans-serif;
37 | font-size: 32px;
38 | /* color: #34353b; */
39 | color: #e7e6e7;
40 | font-weight: 800;
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Nav/Nav.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Nav.css";
3 | import logo from "./../../assets/logo.png";
4 |
5 | const Nav = () => {
6 | return (
7 |
8 |
9 |
10 |
FlashType
11 |
12 |
22 |
23 | );
24 | };
25 |
26 | export default Nav;
27 |
--------------------------------------------------------------------------------
/src/components/TestContainer/TestContainer.css:
--------------------------------------------------------------------------------
1 | .test-container {
2 | background-color: #ffffff;
3 | margin: 64px 0;
4 | margin-left: 10%;
5 | width: 80%;
6 | min-height: 700px;
7 | display: flex;
8 | flex-wrap: wrap;
9 | flex-direction: column;
10 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
11 | }
12 |
13 | .typing-challenge-cont {
14 | display: flex;
15 | flex-grow: 1;
16 | }
17 |
18 | .try-again-cont {
19 | flex-grow: 1;
20 | display: flex;
21 | flex-direction: column;
22 | justify-content: center;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/TestContainer/TestContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TryAgain from "../TryAgain/TryAgain";
3 | import TypingChallengeContainer from "../TypingChallengeContainer/TypingChallengeContainer";
4 | import "./TestContainer.css";
5 |
6 | const TestContainer = ({
7 | selectedParagraph,
8 | testInfo,
9 | onInputChange,
10 | words,
11 | characters,
12 | wpm,
13 | timeRemaining,
14 | timerStarted,
15 | startAgain,
16 | }) => {
17 | return (
18 |
19 | {/* Show the try again or start screen */}
20 | {timeRemaining > 0 ? (
21 |
22 |
32 |
33 | ) : (
34 |
35 |
41 |
42 | )}
43 |
44 | );
45 | };
46 |
47 | export default TestContainer;
48 |
--------------------------------------------------------------------------------
/src/components/TestLetter/TestLetter.css:
--------------------------------------------------------------------------------
1 | .test-letter {
2 | /* font-weight: 800; */
3 | font-size: 19px;
4 | line-height: 19px;
5 | }
6 |
7 | .test-letter-not-attempted {
8 | color: #f9a825;
9 | }
10 |
11 | .test-letter-incorrect {
12 | color: #bf360c;
13 | }
14 |
15 | .test-letter-correct {
16 | color: #558b2f;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/TestLetter/TestLetter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./TestLetter.css";
3 |
4 | const TestLetter = ({ individualLetterInfo }) => {
5 | const statusClassName = {
6 | correct: "test-letter-correct",
7 | incorrect: "test-letter-incorrect",
8 | notAttempted: "test-letter-not-attempted",
9 | }[individualLetterInfo.status];
10 |
11 | return (
12 |
13 | {individualLetterInfo.testLetter}
14 |
15 | );
16 | };
17 |
18 | export default TestLetter;
19 |
--------------------------------------------------------------------------------
/src/components/TryAgain/TryAgain.css:
--------------------------------------------------------------------------------
1 | .try-again-container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | }
7 |
8 | .try-again-container > h1 {
9 | text-decoration: underline;
10 | margin: 0;
11 | font-size: 36px;
12 | margin-bottom: 20px;
13 | }
14 |
15 | .result-container > p {
16 | font-size: 28px;
17 | margin: 7px;
18 | }
19 |
20 | .end-buttons {
21 | position: relative;
22 | margin: 12px;
23 | border: none;
24 | border-radius: 4px;
25 | padding: 2px 16px;
26 | min-width: 64px;
27 | vertical-align: middle;
28 | text-align: center;
29 | color: #fff;
30 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
31 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
32 | font-size: 18px;
33 | font-weight: 500;
34 | line-height: 36px;
35 | cursor: pointer;
36 | transition: background-color 0.2s;
37 | }
38 |
39 | .start-again-btn {
40 | background: #3e4248;
41 | }
42 |
43 | .start-again-btn:hover,
44 | .start-again-btn:focus {
45 | background: #808080;
46 | }
47 |
48 | .share-btn {
49 | background: #3b5998;
50 | }
51 |
52 | .share-btn:hover,
53 | .share-btn:focus {
54 | background: #5d7dc4;
55 | }
56 |
57 | .tweet-btn {
58 | background: #1da1f2;
59 | }
60 |
61 | .tweet-btn:hover,
62 | .tweet-btn:focus {
63 | background: #8faef3;
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/TryAgain/TryAgain.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./TryAgain.css";
3 |
4 | const TryAgain = ({ words, characters, wpm, startAgain }) => {
5 | const url = "theleanprogrammer.com";
6 | return (
7 |
8 |
Test Results
9 |
10 |
11 |
12 | Characters: {characters}
13 |
14 |
15 | Words: {words}
16 |
17 |
18 | Speed {wpm} wpm
19 |
20 |
21 |
22 |
23 | startAgain()}
25 | className="end-buttons start-again-btn"
26 | >
27 | Re-try
28 |
29 |
31 | window.open(
32 | "https://www.facebook.com/sharer/sharer.php?u=" +
33 | url,
34 | "facebook-share-dialog",
35 | "width=800,height=600"
36 | )
37 | }
38 | className="end-buttons share-btn"
39 | >
40 | Share
41 |
42 |
44 | window.open(
45 | "https://twitter.com/intent/tweet?text=Check%20this%20out%20" +
46 | url,
47 | "Twitter",
48 | "width=800,height=600"
49 | )
50 | }
51 | className="end-buttons tweet-btn"
52 | >
53 | Tweet
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default TryAgain;
61 |
--------------------------------------------------------------------------------
/src/components/TypingChallenge/TypingChallenge.css:
--------------------------------------------------------------------------------
1 | .typing-challenge {
2 | display: flex;
3 | flex-direction: column;
4 | flex-grow: 1;
5 | align-items: center;
6 | justify-content: center;
7 | }
8 |
9 | .timer-container {
10 | margin: 16px;
11 | /* margin-bottom: 0; */
12 | }
13 |
14 | .timer {
15 | font-size: 38px;
16 | font-weight: 600;
17 | margin: 0;
18 | text-align: center;
19 | }
20 |
21 | .timer-info {
22 | margin: 0;
23 | margin-top: -5px;
24 | color: #dd5044;
25 | font-size: 20px;
26 | text-align: center;
27 | }
28 |
29 | .textarea-container {
30 | margin: 16px;
31 | display: flex;
32 | flex-direction: row;
33 | flex-grow: 1;
34 | width: 80%;
35 | }
36 |
37 | .textarea-left,
38 | .textarea-right {
39 | display: flex;
40 | width: 50%;
41 | flex-grow: 1;
42 | }
43 |
44 | .textarea {
45 | text-align: left;
46 | flex-grow: 1;
47 | height: 400px;
48 | width: 100%;
49 | padding: 10px;
50 | line-height: 18px;
51 | flex-wrap: wrap;
52 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
53 | transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
54 | overflow: scroll;
55 | }
56 |
57 | .textarea:hover {
58 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
59 | }
60 |
61 | .textarea::-webkit-scrollbar {
62 | display: none;
63 | }
64 |
65 | .textarea {
66 | -ms-overflow-style: none; /* IE and Edge */
67 | scrollbar-width: none; /* Firefox */
68 | }
69 |
70 | .test-paragraph {
71 | font-size: 12px;
72 | background: #e9e7e4;
73 | padding: 12px;
74 | /* display: flex;
75 | flex-direction: row; */
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/TypingChallenge/TypingChallenge.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TestLetter from "../TestLetter/TestLetter";
3 | import "./TypingChallenge.css";
4 |
5 | const TypingChallenge = ({
6 | testInfo,
7 | onInputChange,
8 | timeRemaining,
9 | timerStarted,
10 | }) => {
11 | return (
12 |
13 |
14 |
15 | 00:
16 | {timeRemaining >= 10 ? timeRemaining : `0${timeRemaining}`}
17 |
18 |
19 | {!timerStarted && "Start typing to start the test"}
20 |
21 |
22 |
23 |
24 |
25 |
26 | {/* {selectedParagraph} */}
27 | {testInfo.map((individualLetterInfo, index) => (
28 |
32 | ))}
33 |
34 |
35 |
36 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default TypingChallenge;
48 |
--------------------------------------------------------------------------------
/src/components/TypingChallengeContainer/TypingChallengeContainer.css:
--------------------------------------------------------------------------------
1 | .typing-challenge-container {
2 | display: flex;
3 | flex-direction: column;
4 | flex-grow: 1;
5 | }
6 |
7 | .details-container {
8 | display: flex;
9 | flex-direction: row;
10 | flex-wrap: wrap;
11 | }
12 |
13 | .typewriter-container {
14 | display: flex;
15 | flex-grow: 1;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/TypingChallengeContainer/TypingChallengeContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ChallengeDetailsCard from "../ChallengeDetailsCard/ChallengeDetailsCard";
3 | import TypingChallenge from "../TypingChallenge/TypingChallenge";
4 | import "./TypingChallengeContainer.css";
5 |
6 | const TypingChallengeContainer = ({
7 | selectedParagraph,
8 | testInfo,
9 | onInputChange,
10 | words,
11 | characters,
12 | wpm,
13 | timeRemaining,
14 | timerStarted,
15 | }) => {
16 | return (
17 |
18 |
19 | {/* Words Typed */}
20 |
21 |
22 | {/* Characters Typed */}
23 |
27 |
28 | {/* Mistakes */}
29 |
30 |
31 |
32 |
33 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default TypingChallengeContainer;
46 |
--------------------------------------------------------------------------------
/src/data/sampleParagraphs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fallback paragraphs in case the API doesn't work.
3 | * Paragraphs taken from http://metaphorpsum.com/paragraphs/
4 | */
5 |
6 | export const SAMPLE_PARAGRAPHS = [
7 | "An airbus is a leg from the right perspective. Before pantries, harmonies were only baritones. Few can name a serene mistake that isn't a stockinged sword. Casebook nodes show us how pinks can be houses. This is not to discredit the idea that a key is a distrait interest. A step-grandfather of the carp is assumed to be an algal sunflower. A sneaking pine is a swing of the mind. Some ocher coins are thought of simply as slashes. Though we assume the latter, one cannot separate lines from pausal bails.",
8 | "This could be, or perhaps a collar is the pamphlet of a kimberly. A dad of the design is assumed to be a patent june. A teeny chief is a james of the mind. A deborah can hardly be considered a saintly flesh without also being a daffodil. Those ministers are nothing more than cathedrals. Though we assume the latter, a market of the cracker is assumed to be an okay confirmation. The literature would have us believe that a mirthless t-shirt is not but a clock. The headlong carol comes from a puddly whistle. The olives could be said to resemble upbound ethernets.",
9 | "As far as we can estimate, their hat was, in this moment, a towered pakistan. Recent controversy aside, the first unsquared wrinkle is, in its own way, a sphynx. The lutes could be said to resemble cystoid lipsticks. However, before jennifers, attacks were only irans. The rubbers could be said to resemble broadloom cards. Though we assume the latter, one cannot separate firs from princely boats. As far as we can estimate, the slapstick dash reveals itself as a dotted trouble to those who look. A street is the trail of a daniel. They were lost without the hoyden pasta that composed their stove.",
10 | "A grudging grouse's desk comes with it the thought that the wooded shark is a quality. Before commas, times were only harmonicas. A gum is a wasp's drop. Stopsigns are flabby insects. Fortnights are premorse celestes. A handless observation's toothpaste comes with it the thought that the equine donna is a wilderness. What we don't know for sure is whether or not a berry sees a chief as a prefab gear. The zeitgeist contends that those barges are nothing more than taxes. Framed in a different way, a leopard is a supply's chin.",
11 | "One cannot separate kohlrabis from bobtail trails. Their jail was, in this moment, a yearling belief. The pint of a rowboat becomes a venous scarecrow. In recent years, the orchid of a harbor becomes a jointed lake. They were lost without the bogus trunk that composed their adult. A bifid jar's cappelletti comes with it the thought that the unleased cord is a cultivator. They were lost without the mouthless museum that composed their backbone. Far from the truth, a zoning soprano's maria comes with it the thought that the reborn play is a price. A kick sees a reindeer as a stolen archaeology.",
12 | "To be more specific, few can name a blameful shelf that isn't an unborn airbus. Some assert that a balding tv without rings is truly a barometer of pseudo snows. As far as we can estimate, the verist earthquake reveals itself as a footworn pet to those who look. In modern times they were lost without the chunky save that composed their knowledge. Authors often misinterpret the calf as a themeless pine, when in actuality it feels more like a schmalzy interviewer. We know that the octopi could be said to resemble sparry baseballs. The gutless gray reveals itself as an unfiled flood to those who look. An oyster sees a motorcycle as a neuron pharmacist. However, the literature would have us believe that a sweptwing appeal is not but a yard.",
13 | "We can assume that any instance of a stool can be construed as a funest handle. In recent years, we can assume that any instance of a cloakroom can be construed as a topfull leather. Far from the truth, a turnip sees a man as a churlish poison. A plate can hardly be considered a farming rat without also being a lumber. Some posit the cissoid pastor to be less than shamefaced. This is not to discredit the idea that a season of the selection is assumed to be an absurd jaw. As far as we can estimate, the time of a level becomes a prolate october. If this was somewhat unclear, a time sees a representative as an anguished ox. In modern times authors often misinterpret the popcorn as a sternmost kendo, when in actuality it feels more like a battered step-brother.",
14 | "Some posit the thickset timer to be less than shaken. A methane of the deodorant is assumed to be a snappish cold. The toeless blanket comes from a learned clover. A hollow command is an island of the mind. In recent years, the whity snowboard comes from a bijou cause. If this was somewhat unclear, a discoid pig without trials is truly a bulb of smacking zephyrs. Few can name a luscious honey that isn't a deuced guilty. Before undercloths, siameses were only capitals. Some assert that few can name a sunfast edger that isn't a smectic laura.",
15 | "The montane peripheral comes from a hoggish security. Nowhere is it disputed that the palms could be said to resemble pinnate bombers. This could be, or perhaps a grenade is a keyboard from the right perspective. Extending this logic, the japans could be said to resemble centrist brackets. Their Friday was, in this moment, a phatic helmet. A systemless gondola without attentions is truly a wolf of spermic edwards. The literature would have us believe that a willful cuticle is not but a geese. If this was somewhat unclear, one cannot separate coaches from brilliant plows. A dish is an olden baritone.",
16 | "Some chairborne fronts are thought of simply as routes. One cannot separate hamsters from crackbrained journeies. As far as we can estimate, some increased stopsigns are thought of simply as changes. Few can name an uncalled doctor that isn't a ledgy kendo. Some posit the dendroid buffet to be less than surgy. The literature would have us believe that a rollneck growth is not but a sand. They were lost without the vying bulb that composed their blouse. A heat is a watchmaker's white. The fitchy bush reveals itself as a briny share to those who look.",
17 | "To be more specific, the grades could be said to resemble latish plasterboards. The first strawless grain is, in its own way, an undercloth. A sheep is the time of a step-son. Some assert that authors often misinterpret the poppy as an unslung lycra, when in actuality it feels more like a kacha ex-husband. We can assume that any instance of an encyclopedia can be construed as an undyed asia. We know that feathers are humdrum reactions. It's an undeniable fact, really; before communities, ashes were only ATMS. The first dicky delete is, in its own way, a cross. They were lost without the arranged string that composed their paint.",
18 | "Extending this logic, the basses could be said to resemble zippy sugars. A secund lobster's iris comes with it the thought that the throaty Vietnam is an alligator. A toad is the kitchen of a production. Some posit the notour chef to be less than morish. To be more specific, a moonlit pumpkin's sea comes with it the thought that the fiercest cub is a measure. Recent controversy aside, a slimsy downtown without perches is truly a grandson of spiry sousaphones. The bowl of a couch becomes a daring archeology. A kayak is a chill from the right perspective. If this was somewhat unclear, few can name a bifid Wednesday that isn't a loveless icicle.",
19 | "Framed in a different way, a nimble insurance is a rise of the mind. A rice of the aftershave is assumed to be a contained sink. As far as we can estimate, the unkind fiction comes from a seasick latex. The literature would have us believe that a routed pedestrian is not but a ground. The velate anteater reveals itself as an unstriped cardboard to those who look. The musician of a train becomes an upward ease. It's an undeniable fact, really; a snugger toy is a romanian of the mind. Some fleshy raies are thought of simply as selections. An oval of the baby is assumed to be a shiny room.",
20 | "The literature would have us believe that an upgrade workshop is not but a tabletop. A sissy liquor without bails is truly a school of awheel dungeons. Shaded landmines show us how violets can be augusts. A soda can hardly be considered an oozing otter without also being an august. One cannot separate bottoms from wrinkly periods. A math is the kilogram of a centimeter. To be more specific, a hyena is the name of a stretch. However, the bridgeless fan comes from an elapsed cucumber. Before faucets, waitresses were only parties.",
21 | ];
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./components/App/App";
4 |
5 | ReactDOM.render( , document.getElementById("root"));
6 |
--------------------------------------------------------------------------------