Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/data/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CatHttpCode } from "../pages/activities/CatHttpCode";
import FlappyBird from "../pages/games/FlappyBird";
import RandomIdentity from "../pages/activities/RandomIdentity";
import word_scramble_icon from "../assets/games/WordScramble/word_scramble.png";
import TypingTest from "../pages/games/TypingTest";

export const activities = [
{
Expand Down Expand Up @@ -179,5 +180,13 @@ export const games = [
icon: word_scramble_icon,
urlTerm: "WordScramble",
element: <WordScramble />,
},
{
title: "Typing Test",
description: "Test your typing skills",
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",
urlTerm:"TypingTest",
element:<TypingTest/>,

}
];
166 changes: 166 additions & 0 deletions src/pages/games/TypingTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useEffect, useMemo, useState } from "react";
import "../../styles/pages/games/TypingTest.css";

const SAMPLE_PARAGRAPHS = [
"The quick brown fox jumps over the lazy dog.",
"Typing is a skill that improves with focus and consistency.",
"React makes it painless to create interactive user interfaces.",
"Code slowly, read carefully, and test your changes.",
"Practice daily to increase your speed and accuracy."
];

function getRandomParagraph() {
const idx = Math.floor(Math.random() * SAMPLE_PARAGRAPHS.length);
return SAMPLE_PARAGRAPHS[idx];
}

export default function TypingTest() {
const [targetText, setTargetText] = useState(getRandomParagraph);
const [typedText, setTypedText] = useState("");
const [started, setStarted] = useState(false);
const [time, setTime] = useState(0);
const [finished, setFinished] = useState(false);
const [bestWpm, setBestWpm] = useState(() => {
const fromLS = localStorage.getItem("typing_best_wpm");
return fromLS ? Number(fromLS) : 0;
});

// timer: start on first key, stop on finish
useEffect(() => {
let timer;
if (started && !finished) {
timer = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
} else {
if (timer) clearInterval(timer);
}
return () => {
if (timer) clearInterval(timer);
};
}, [started, finished]);

// derived metrics
const stats = useMemo(() => {
const wordsTyped =
typedText.trim().length === 0
? 0
: typedText.trim().split(/\s+/).length;
const minutes = time / 60;
const wpm = minutes > 0 ? Math.round(wordsTyped / minutes) : 0;

// accuracy
let correct = 0;
for (let i = 0; i < typedText.length; i++) {
if (typedText[i] === targetText[i]) correct++;
}
const accuracy =
typedText.length === 0
? 100
: Math.round((correct / typedText.length) * 100);

return { wpm, accuracy, wordsTyped };
}, [typedText, time, targetText]);

// when completed, stop and save best
useEffect(() => {
if (typedText === targetText && targetText.length > 0) {
setFinished(true);
setStarted(false);
if (stats.wpm > bestWpm) {
setBestWpm(stats.wpm);
localStorage.setItem("typing_best_wpm", String(stats.wpm));
}
}
}, [typedText, targetText, stats.wpm, bestWpm]);

function handleChange(e) {
const val = e.target.value;

// auto start on first key
if (!started && !finished) {
setStarted(true);
}

// restrict to paragraph length
if (val.length <= targetText.length) {
setTypedText(val);

// auto end on completion
if (val === targetText) {
setFinished(true);
setStarted(false);
}
}
}

function handleRestart() {
const nextPara = getRandomParagraph();
setTargetText(nextPara);
setTypedText("");
setTime(0);
setFinished(false);
setStarted(false);
}

return (
<div className="typing-test-container">
<div className="typing-header">
<h2>Typing Speed Test</h2>
<p>Type the text below as fast and as accurately as you can.</p>
</div>

<div className="typing-stats">
<div className="stat-card">
<span className="stat-label">Time</span>
<span className="stat-value">{time}s</span>
</div>
<div className="stat-card">
<span className="stat-label">WPM</span>
<span className="stat-value">{stats.wpm}</span>
</div>
<div className="stat-card">
<span className="stat-label">Accuracy</span>
<span className="stat-value">{stats.accuracy}%</span>
</div>
<div className="stat-card">
<span className="stat-label">Best WPM</span>
<span className="stat-value">{bestWpm}</span>
</div>
</div>

<div className="typing-target">
{targetText.split("").map((ch, idx) => {
let cls = "";
if (idx < typedText.length) {
cls = typedText[idx] === ch ? "correct" : "incorrect";
}
return (
<span key={idx} className={cls}>
{ch}
</span>
);
})}
</div>

<textarea
className="typing-input"
value={typedText}
onChange={handleChange}
disabled={finished}
placeholder="Start typing here..."
/>

<div className="typing-actions">
<button onClick={handleRestart} className="typing-btn">
Restart
</button>
{finished && (
<span className="typing-done">
✅ Paragraph completed! Final WPM: {stats.wpm}
</span>
)}
</div>
</div>
);
}
169 changes: 169 additions & 0 deletions src/styles/pages/games/TypingTest.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
.typing-test-container {
max-width: 750px;
margin: 2rem auto;
padding: 2rem;
background: #ffffff;
border-radius: 18px;
border: 1px solid #e2e8f0;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.06);
font-family: "Poppins", "Segoe UI", system-ui, sans-serif;
transition: all 0.25s ease;
}

.typing-test-container:hover {
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.09);
border-color: #cbd5e1;
}

.typing-header {
text-align: center;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 1rem;
}

.typing-header h2 {
font-size: 1.8rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.3rem;
}

.typing-header p {
color: #64748b;
font-size: 0.9rem;
}

.typing-stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-top: 1.3rem;
}

.stat-card {
background: #f9fafb;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 0.8rem 1.1rem;
text-align: center;
width: 150px;
margin: 0.4rem;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.08);
}

.stat-label {
display: block;
font-size: 0.7rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.03em;
}

.stat-value {
font-size: 1.3rem;
font-weight: 600;
color: #0f172a;
}

.typing-target {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 1rem;
margin-top: 1.5rem;
font-family: "JetBrains Mono", monospace;
line-height: 1.6;
font-size: 1rem;
color: #0f172a;
min-height: 90px;
letter-spacing: 0.02em;
}

.typing-target span.correct {
background: rgba(34, 197, 94, 0.08);
color: #15803d;
border-radius: 4px;
}

.typing-target span.incorrect {
background: rgba(244, 63, 94, 0.1);
color: #b91c1c;
text-decoration: line-through;
border-radius: 4px;
}

.typing-input {
margin-top: 1.3rem;
width: 100%;
min-height: 120px;
border-radius: 10px;
border: 1px solid #cbd5e1;
padding: 0.9rem 1rem;
font-size: 0.95rem;
color: #1e293b;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.4;
resize: vertical;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.typing-input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}

.typing-actions {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 1.2rem;
flex-wrap: wrap;
}

.typing-btn {
background: linear-gradient(135deg, #2563eb, #3b82f6);
color: #fff;
border: none;
border-radius: 10px;
padding: 0.6rem 1.3rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.25);
}

.typing-btn:hover {
background: linear-gradient(135deg, #1d4ed8, #2563eb);
transform: translateY(-2px);
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.3);
}

.typing-done {
font-size: 0.9rem;
color: #16a34a;
font-weight: 600;
text-align: right;
flex: 1;
}

@media (max-width: 640px) {
.typing-stats {
flex-direction: column;
align-items: center;
}
.stat-card {
width: 80%;
}
.typing-actions {
flex-direction: column;
align-items: flex-start;
gap: 0.6rem;
}
}