Skip to content

Commit 7efab39

Browse files
Merge pull request #129 from akshat-chd/Typing
Typing Test
2 parents e64af13 + 3f84525 commit 7efab39

File tree

3 files changed

+344
-0
lines changed

3 files changed

+344
-0
lines changed

src/data/content.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CatHttpCode } from "../pages/activities/CatHttpCode";
2828
import FlappyBird from "../pages/games/FlappyBird";
2929
import RandomIdentity from "../pages/activities/RandomIdentity";
3030
import word_scramble_icon from "../assets/games/WordScramble/word_scramble.png";
31+
import TypingTest from "../pages/games/TypingTest";
3132

3233
export const activities = [
3334
{
@@ -179,5 +180,13 @@ export const games = [
179180
icon: word_scramble_icon,
180181
urlTerm: "WordScramble",
181182
element: <WordScramble />,
183+
},
184+
{
185+
title: "Typing Test",
186+
description: "Test your typing skills",
187+
icon :"https://typingmentor.com/_next/image/?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fbs3wcomf%2Fproduction%2F9f626374aa1c388a6418a077990771aff53d054f-1200x800.jpg%3Frect%3D0%2C35%2C1200%2C731%26w%3D808%26h%3D492%26fit%3Dcrop%26auto%3Dformat&w=1920&q=75",
188+
urlTerm:"TypingTest",
189+
element:<TypingTest/>,
190+
182191
}
183192
];

src/pages/games/TypingTest.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import React, { useEffect, useMemo, useState } from "react";
2+
import "../../styles/pages/games/TypingTest.css";
3+
4+
const SAMPLE_PARAGRAPHS = [
5+
"The quick brown fox jumps over the lazy dog.",
6+
"Typing is a skill that improves with focus and consistency.",
7+
"React makes it painless to create interactive user interfaces.",
8+
"Code slowly, read carefully, and test your changes.",
9+
"Practice daily to increase your speed and accuracy."
10+
];
11+
12+
function getRandomParagraph() {
13+
const idx = Math.floor(Math.random() * SAMPLE_PARAGRAPHS.length);
14+
return SAMPLE_PARAGRAPHS[idx];
15+
}
16+
17+
export default function TypingTest() {
18+
const [targetText, setTargetText] = useState(getRandomParagraph);
19+
const [typedText, setTypedText] = useState("");
20+
const [started, setStarted] = useState(false);
21+
const [time, setTime] = useState(0);
22+
const [finished, setFinished] = useState(false);
23+
const [bestWpm, setBestWpm] = useState(() => {
24+
const fromLS = localStorage.getItem("typing_best_wpm");
25+
return fromLS ? Number(fromLS) : 0;
26+
});
27+
28+
// timer: start on first key, stop on finish
29+
useEffect(() => {
30+
let timer;
31+
if (started && !finished) {
32+
timer = setInterval(() => {
33+
setTime((t) => t + 1);
34+
}, 1000);
35+
} else {
36+
if (timer) clearInterval(timer);
37+
}
38+
return () => {
39+
if (timer) clearInterval(timer);
40+
};
41+
}, [started, finished]);
42+
43+
// derived metrics
44+
const stats = useMemo(() => {
45+
const wordsTyped =
46+
typedText.trim().length === 0
47+
? 0
48+
: typedText.trim().split(/\s+/).length;
49+
const minutes = time / 60;
50+
const wpm = minutes > 0 ? Math.round(wordsTyped / minutes) : 0;
51+
52+
// accuracy
53+
let correct = 0;
54+
for (let i = 0; i < typedText.length; i++) {
55+
if (typedText[i] === targetText[i]) correct++;
56+
}
57+
const accuracy =
58+
typedText.length === 0
59+
? 100
60+
: Math.round((correct / typedText.length) * 100);
61+
62+
return { wpm, accuracy, wordsTyped };
63+
}, [typedText, time, targetText]);
64+
65+
// when completed, stop and save best
66+
useEffect(() => {
67+
if (typedText === targetText && targetText.length > 0) {
68+
setFinished(true);
69+
setStarted(false);
70+
if (stats.wpm > bestWpm) {
71+
setBestWpm(stats.wpm);
72+
localStorage.setItem("typing_best_wpm", String(stats.wpm));
73+
}
74+
}
75+
}, [typedText, targetText, stats.wpm, bestWpm]);
76+
77+
function handleChange(e) {
78+
const val = e.target.value;
79+
80+
// auto start on first key
81+
if (!started && !finished) {
82+
setStarted(true);
83+
}
84+
85+
// restrict to paragraph length
86+
if (val.length <= targetText.length) {
87+
setTypedText(val);
88+
89+
// auto end on completion
90+
if (val === targetText) {
91+
setFinished(true);
92+
setStarted(false);
93+
}
94+
}
95+
}
96+
97+
function handleRestart() {
98+
const nextPara = getRandomParagraph();
99+
setTargetText(nextPara);
100+
setTypedText("");
101+
setTime(0);
102+
setFinished(false);
103+
setStarted(false);
104+
}
105+
106+
return (
107+
<div className="typing-test-container">
108+
<div className="typing-header">
109+
<h2>Typing Speed Test</h2>
110+
<p>Type the text below as fast and as accurately as you can.</p>
111+
</div>
112+
113+
<div className="typing-stats">
114+
<div className="stat-card">
115+
<span className="stat-label">Time</span>
116+
<span className="stat-value">{time}s</span>
117+
</div>
118+
<div className="stat-card">
119+
<span className="stat-label">WPM</span>
120+
<span className="stat-value">{stats.wpm}</span>
121+
</div>
122+
<div className="stat-card">
123+
<span className="stat-label">Accuracy</span>
124+
<span className="stat-value">{stats.accuracy}%</span>
125+
</div>
126+
<div className="stat-card">
127+
<span className="stat-label">Best WPM</span>
128+
<span className="stat-value">{bestWpm}</span>
129+
</div>
130+
</div>
131+
132+
<div className="typing-target">
133+
{targetText.split("").map((ch, idx) => {
134+
let cls = "";
135+
if (idx < typedText.length) {
136+
cls = typedText[idx] === ch ? "correct" : "incorrect";
137+
}
138+
return (
139+
<span key={idx} className={cls}>
140+
{ch}
141+
</span>
142+
);
143+
})}
144+
</div>
145+
146+
<textarea
147+
className="typing-input"
148+
value={typedText}
149+
onChange={handleChange}
150+
disabled={finished}
151+
placeholder="Start typing here..."
152+
/>
153+
154+
<div className="typing-actions">
155+
<button onClick={handleRestart} className="typing-btn">
156+
Restart
157+
</button>
158+
{finished && (
159+
<span className="typing-done">
160+
✅ Paragraph completed! Final WPM: {stats.wpm}
161+
</span>
162+
)}
163+
</div>
164+
</div>
165+
);
166+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
.typing-test-container {
2+
max-width: 750px;
3+
margin: 2rem auto;
4+
padding: 2rem;
5+
background: #ffffff;
6+
border-radius: 18px;
7+
border: 1px solid #e2e8f0;
8+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.06);
9+
font-family: "Poppins", "Segoe UI", system-ui, sans-serif;
10+
transition: all 0.25s ease;
11+
}
12+
13+
.typing-test-container:hover {
14+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.09);
15+
border-color: #cbd5e1;
16+
}
17+
18+
.typing-header {
19+
text-align: center;
20+
border-bottom: 1px solid #e2e8f0;
21+
padding-bottom: 1rem;
22+
}
23+
24+
.typing-header h2 {
25+
font-size: 1.8rem;
26+
font-weight: 700;
27+
color: #1e293b;
28+
margin-bottom: 0.3rem;
29+
}
30+
31+
.typing-header p {
32+
color: #64748b;
33+
font-size: 0.9rem;
34+
}
35+
36+
.typing-stats {
37+
display: flex;
38+
justify-content: space-around;
39+
flex-wrap: wrap;
40+
margin-top: 1.3rem;
41+
}
42+
43+
.stat-card {
44+
background: #f9fafb;
45+
border: 1px solid #e2e8f0;
46+
border-radius: 12px;
47+
padding: 0.8rem 1.1rem;
48+
text-align: center;
49+
width: 150px;
50+
margin: 0.4rem;
51+
transition: transform 0.15s ease, box-shadow 0.15s ease;
52+
}
53+
54+
.stat-card:hover {
55+
transform: translateY(-3px);
56+
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.08);
57+
}
58+
59+
.stat-label {
60+
display: block;
61+
font-size: 0.7rem;
62+
color: #94a3b8;
63+
text-transform: uppercase;
64+
letter-spacing: 0.03em;
65+
}
66+
67+
.stat-value {
68+
font-size: 1.3rem;
69+
font-weight: 600;
70+
color: #0f172a;
71+
}
72+
73+
.typing-target {
74+
background: #f8fafc;
75+
border: 1px solid #e2e8f0;
76+
border-radius: 12px;
77+
padding: 1rem;
78+
margin-top: 1.5rem;
79+
font-family: "JetBrains Mono", monospace;
80+
line-height: 1.6;
81+
font-size: 1rem;
82+
color: #0f172a;
83+
min-height: 90px;
84+
letter-spacing: 0.02em;
85+
}
86+
87+
.typing-target span.correct {
88+
background: rgba(34, 197, 94, 0.08);
89+
color: #15803d;
90+
border-radius: 4px;
91+
}
92+
93+
.typing-target span.incorrect {
94+
background: rgba(244, 63, 94, 0.1);
95+
color: #b91c1c;
96+
text-decoration: line-through;
97+
border-radius: 4px;
98+
}
99+
100+
.typing-input {
101+
margin-top: 1.3rem;
102+
width: 100%;
103+
min-height: 120px;
104+
border-radius: 10px;
105+
border: 1px solid #cbd5e1;
106+
padding: 0.9rem 1rem;
107+
font-size: 0.95rem;
108+
color: #1e293b;
109+
font-family: "Inter", system-ui, sans-serif;
110+
line-height: 1.4;
111+
resize: vertical;
112+
outline: none;
113+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
114+
}
115+
116+
.typing-input:focus {
117+
border-color: #2563eb;
118+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
119+
}
120+
121+
.typing-actions {
122+
display: flex;
123+
align-items: center;
124+
justify-content: space-between;
125+
margin-top: 1.2rem;
126+
flex-wrap: wrap;
127+
}
128+
129+
.typing-btn {
130+
background: linear-gradient(135deg, #2563eb, #3b82f6);
131+
color: #fff;
132+
border: none;
133+
border-radius: 10px;
134+
padding: 0.6rem 1.3rem;
135+
font-size: 0.9rem;
136+
font-weight: 500;
137+
cursor: pointer;
138+
transition: all 0.2s ease;
139+
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.25);
140+
}
141+
142+
.typing-btn:hover {
143+
background: linear-gradient(135deg, #1d4ed8, #2563eb);
144+
transform: translateY(-2px);
145+
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.3);
146+
}
147+
148+
.typing-done {
149+
font-size: 0.9rem;
150+
color: #16a34a;
151+
font-weight: 600;
152+
text-align: right;
153+
flex: 1;
154+
}
155+
156+
@media (max-width: 640px) {
157+
.typing-stats {
158+
flex-direction: column;
159+
align-items: center;
160+
}
161+
.stat-card {
162+
width: 80%;
163+
}
164+
.typing-actions {
165+
flex-direction: column;
166+
align-items: flex-start;
167+
gap: 0.6rem;
168+
}
169+
}

0 commit comments

Comments
 (0)