Skip to content

Commit 17a30f1

Browse files
committed
feat: FezType
1 parent 5bb70c4 commit 17a30f1

File tree

3 files changed

+286
-1
lines changed

3 files changed

+286
-1
lines changed

public/apps/apps.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
"icon": "GameControllerIcon",
66
"order": 1,
77
"apps": [
8+
{
9+
"slug": "feztype",
10+
"to": "/apps/feztype",
11+
"title": "FezType (Typing Speed Tester)",
12+
"description": "Test and improve your typing speed.",
13+
"icon": "KeyboardIcon",
14+
"created_at": "2025-11-10T14:00:00+03:00"
15+
},
816
{
917
"slug": "card-game",
1018
"to": "/apps/card-game",
@@ -447,4 +455,4 @@
447455
}
448456
]
449457
}
450-
}
458+
}

src/components/AnimatedRoutes.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import WhiteboardPage from '../pages/apps/WhiteboardPage';
7070
import FootballEmblemCreatorPage from '../pages/apps/FootballEmblemCreatorPage';
7171
import RoguelikeGamePage from '../pages/apps/RoguelikeGamePage'; // Import RoguelikeGamePage
7272
import TcgCardGeneratorPage from '../pages/apps/TcgCardGeneratorPage'; // Import TcgCardGeneratorPage
73+
import KeyboardTypingSpeedTesterPage from '../pages/apps/KeyboardTypingSpeedTesterPage'; // Import KeyboardTypingSpeedTesterPage
7374
import SettingsPage from '../pages/SettingsPage';
7475

7576
import UsefulLinksPage from '../pages/UsefulLinksPage';
@@ -638,6 +639,10 @@ function AnimatedRoutes() {
638639
path="/apps::tcg"
639640
element={<Navigate to="/apps/tcg-card-generator" replace />}
640641
/>
642+
<Route
643+
path="/apps::ft"
644+
element={<Navigate to="/apps/feztype" replace />}
645+
/>
641646
{/* End of hardcoded redirects */}
642647
<Route
643648
path="/apps/ip"
@@ -653,6 +658,20 @@ function AnimatedRoutes() {
653658
</motion.div>
654659
}
655660
/>
661+
<Route
662+
path="/apps/feztype"
663+
element={
664+
<motion.div
665+
initial="initial"
666+
animate="in"
667+
exit="out"
668+
variants={pageVariants}
669+
transition={pageTransition}
670+
>
671+
<KeyboardTypingSpeedTesterPage />
672+
</motion.div>
673+
}
674+
/>
656675
<Route
657676
path="/apps/tcg-card-generator"
658677
element={
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import React, {useState, useEffect, useRef, useCallback} from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {ArrowLeftIcon, KeyboardIcon} from '@phosphor-icons/react';
4+
import useSeo from '../../hooks/useSeo';
5+
import BreadcrumbTitle from '../../components/BreadcrumbTitle';
6+
7+
const sampleTexts = ["The quick brown fox jumps over the lazy dog.", "Never underestimate the power of a good book.", "Coding is like poetry; it should be beautiful and efficient.", "The early bird catches the worm, but the second mouse gets the cheese.", "Innovation distinguishes between a leader and a follower.", "Success is not final, failure is not fatal: it is the courage to continue that counts."];
8+
9+
function KeyboardTypingSpeedTesterPage() {
10+
const appName = "FezType"; // Renamed as per user's request
11+
const appSlug = "feztype"; // Corresponding slug
12+
13+
useSeo({
14+
title: `${appName} | Fezcodex`,
15+
description: "Test and improve your typing speed with FezType.",
16+
keywords: ['Fezcodex', 'typing test', 'wpm', 'typing speed', 'keyboard', 'games'],
17+
ogTitle: `${appName} | Fezcodex`,
18+
ogDescription: "Test and improve your typing speed with FezType.",
19+
ogImage: 'https://fezcode.github.io/logo512.png',
20+
twitterCard: 'summary_large_image',
21+
twitterTitle: `${appName} | Fezcodex`,
22+
twitterDescription: "Test and improve your typing speed with FezType.",
23+
twitterImage: 'https://fezcode.github.io/logo512.png',
24+
});
25+
26+
const [textToType, setTextToType] = useState('');
27+
const [typedText, setTypedText] = useState('');
28+
const [timer, setTimer] = useState(60); // 60 seconds for the test
29+
const [timerActive, setTimerActive] = useState(false);
30+
const [testStarted, setTestStarted] = useState(false);
31+
const [testCompleted, setTestCompleted] = useState(false);
32+
const [wpm, setWpm] = useState(0);
33+
const [accuracy, setAccuracy] = useState(0);
34+
const [mistakes, setMistakes] = useState(0); // Re-added this line
35+
// New states for cumulative tracking
36+
const [totalCorrectChars, setTotalCorrectChars] = useState(0);
37+
const [totalTypedChars, setTotalTypedChars] = useState(0);
38+
const [totalMistakes, setTotalMistakes] = useState(0);
39+
const [textsCompletedCount, setTextsCompletedCount] = useState(0);
40+
41+
const inputRef = useRef(null);
42+
43+
const selectNewText = useCallback((currentText) => { // Takes currentText as argument
44+
let newRandomIndex;
45+
let newText;
46+
do {
47+
newRandomIndex = Math.floor(Math.random() * sampleTexts.length);
48+
newText = sampleTexts[newRandomIndex];
49+
} while (newText === currentText);
50+
51+
return newText;
52+
}, []); // No dependencies
53+
54+
useEffect(() => {
55+
setTextToType(selectNewText('')); // Initial text selection on mount, no previous text
56+
}, [selectNewText]);
57+
58+
const calculateResults = useCallback(() => {
59+
// Overall WPM: (Total correct characters / 5) / Total time elapsed
60+
const timeElapsedMinutes = (60 - timer) / 60; // Total time elapsed (fixed at 60s for now)
61+
const calculatedWpm = timeElapsedMinutes === 0 ? 0 : (totalCorrectChars / 5) / timeElapsedMinutes;
62+
// Overall Accuracy: (Total correct characters / Total typed characters) * 100
63+
const calculatedAccuracy = totalTypedChars === 0 ? 0 : (totalCorrectChars / totalTypedChars) * 100;
64+
65+
setWpm(Math.round(calculatedWpm));
66+
setAccuracy(calculatedAccuracy.toFixed(2));
67+
}, [totalCorrectChars, totalTypedChars, timer]); // Dependencies for overall calculation
68+
69+
useEffect(() => {
70+
let interval = null;
71+
if (timerActive && timer > 0) {
72+
interval = setInterval(() => {
73+
setTimer((prevTimer) => prevTimer - 1);
74+
}, 1000);
75+
} else if (timer === 0 && timerActive) {
76+
setTimerActive(false);
77+
setTestCompleted(true);
78+
calculateResults();
79+
}
80+
return () => clearInterval(interval);
81+
}, [timerActive, timer, calculateResults]);
82+
83+
const handleInputChange = (event) => {
84+
if (testCompleted) return; // Prevent typing after test is completed
85+
86+
if (!testStarted) {
87+
setTestStarted(true);
88+
setTimerActive(true);
89+
}
90+
91+
const newTypedText = event.target.value;
92+
93+
// Update segment-specific mistake tracking (used for rendering visual feedback)
94+
let currentSegmentMistakes = 0;
95+
for (let i = 0; i < newTypedText.length; i++) {
96+
if (i >= textToType.length || newTypedText[i] !== textToType[i]) {
97+
currentSegmentMistakes++;
98+
}
99+
}
100+
// Note: this 'mistakes' state is only for current visual feedback, not cumulative
101+
// The cumulative totalMistakes will be updated when a segment is completed.
102+
setMistakes(currentSegmentMistakes);
103+
setTypedText(newTypedText);
104+
105+
// If typed text length matches the text to type length, process segment completion
106+
if (newTypedText.length === textToType.length) {
107+
// Calculate results for the just-completed text segment
108+
let segmentCorrectChars = 0;
109+
for (let i = 0; i < textToType.length; i++) {
110+
if (newTypedText[i] === textToType[i]) {
111+
segmentCorrectChars++;
112+
}
113+
}
114+
115+
// Update cumulative stats
116+
setTotalCorrectChars(prev => prev + segmentCorrectChars);
117+
setTotalTypedChars(prev => prev + textToType.length);
118+
setTotalMistakes(prev => prev + (textToType.length - segmentCorrectChars));
119+
setTextsCompletedCount(prev => prev + 1);
120+
121+
// Immediately select new text and clear typed input for the next segment
122+
setTextToType((prevText) => selectNewText(prevText));
123+
setTypedText(''); // Clear typed input for the new segment
124+
setMistakes(0); // Reset segment mistakes for the new text
125+
return; // Exit handleInputChange as this segment is complete
126+
}
127+
};
128+
129+
const resetTest = () => {
130+
setTestCompleted(false);
131+
setTestStarted(false);
132+
setTimerActive(false);
133+
setTimer(60);
134+
setWpm(0);
135+
setAccuracy(0);
136+
setTotalCorrectChars(0);
137+
setTotalTypedChars(0);
138+
setTotalMistakes(0);
139+
setTextsCompletedCount(0);
140+
setTextToType((prevText) => {
141+
return selectNewText(prevText);
142+
});
143+
setTypedText('');
144+
setMistakes(0); // Reset segment mistakes for the new test
145+
if (inputRef.current) {
146+
inputRef.current.focus();
147+
}
148+
};
149+
150+
const renderTextToType = () => {
151+
return textToType.split('').map((char, index) => {
152+
let colorClass = 'text-gray-400'; // Default color for untyped characters
153+
let extraClass = '';
154+
155+
if (index < typedText.length) {
156+
// Character has been typed
157+
if (char === typedText[index]) {
158+
// Correct character
159+
if (char === ' ') {
160+
extraClass = 'bg-green-700 bg-opacity-30 rounded-sm'; // Subtle green for correct space
161+
} else {
162+
colorClass = 'text-green-400';
163+
}
164+
} else {
165+
// Incorrect character
166+
if (char === ' ') {
167+
extraClass = 'bg-red-700 bg-opacity-30 rounded-sm'; // Subtle red for incorrect space
168+
} else {
169+
colorClass = 'text-red-400';
170+
}
171+
}
172+
} else if (index === typedText.length && !testCompleted) {
173+
// This is the character the user is currently supposed to type
174+
colorClass = 'text-yellow-400 font-bold'; // Highlight next character to type
175+
}
176+
177+
// Add a visual indicator for the cursor if it's at the current typing position
178+
const isCursorPosition = index === typedText.length && !testCompleted;
179+
180+
return (
181+
<span key={index}
182+
className={`${colorClass} ${extraClass} ${isCursorPosition ? 'border-b-2 border-yellow-400' : ''}`}>
183+
{char === ' ' ? '\u00A0' : char} {/* Use non-breaking space for visual representation of space */}
184+
</span>
185+
);
186+
});
187+
};
188+
return (
189+
<div className="py-16 sm:py-24">
190+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
191+
<Link
192+
to="/apps"
193+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
194+
>
195+
<ArrowLeftIcon size={24}/> Back to Apps
196+
</Link>
197+
<BreadcrumbTitle title={appName} slug={appSlug}/>
198+
<hr className="border-gray-700"/>
199+
<div className="flex justify-center items-center mt-16">
200+
<div
201+
className="bg-app-alpha-10 border-app-alpha-50 text-app group border rounded-lg shadow-2xl p-6 flex flex-col justify-between relative transform transition-all duration-300 ease-in-out scale-105 overflow-hidden h-full w-full max-w-4xl">
202+
<div
203+
className="absolute top-0 left-0 w-full h-full opacity-10"
204+
style={{
205+
backgroundImage: 'radial-gradient(circle, white 1px, transparent 1px)', backgroundSize: '10px 10px',
206+
}}
207+
></div>
208+
<div className="relative z-10 p-1">
209+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app">
210+
<KeyboardIcon size={32} className="inline-block mr-2"/> {appName}
211+
</h1>
212+
<hr className="border-gray-700 mb-4"/>
213+
214+
{!testCompleted ? (<>
215+
<div
216+
className="text-xl font-mono p-4 mb-4 border border-app-alpha-50 rounded-md bg-gray-900/50 min-h-[100px] flex flex-wrap content-start">
217+
{renderTextToType()}
218+
</div>
219+
<input
220+
ref={inputRef}
221+
type="text"
222+
className="w-full p-4 mb-4 bg-gray-800 border border-app-alpha-50 rounded-md focus:ring-0 text-app-light font-mono"
223+
value={typedText}
224+
onChange={handleInputChange}
225+
placeholder="Start typing here..."
226+
disabled={timer === 0 && testStarted}
227+
autoFocus
228+
/>
229+
<div className="font-mono flex justify-between items-center text-lg text-app">
230+
<span>Time: {timer}s</span>
231+
<span>Mistakes: {totalMistakes}</span>
232+
<button
233+
onClick={resetTest}
234+
className="font-mono px-4 py-2 border border-white bg-black/50 hover:bg-white/50 hover:text-black hover:border-black rounded text-white transition-colors"
235+
>
236+
Reset
237+
</button>
238+
</div>
239+
</>) : (<div className="text-center font-mono">
240+
<h2 className="text-4xl font-arvo mb-4 mt-6 text-green-400">Test Complete!</h2>
241+
<p className="text-2xl mb-2">WPM: <span className="font-bold text-app">{wpm}</span></p>
242+
<p className="text-2xl mb-4">Accuracy: <span className="font-bold text-app">{accuracy}%</span></p>
243+
<button
244+
onClick={resetTest}
245+
className="px-6 py-3 border border-white bg-black/50 hover:bg-white/50 hover:text-black hover:border-black rounded text-white text-xl transition-colors"
246+
>
247+
Try Again
248+
</button>
249+
</div>)}
250+
</div>
251+
</div>
252+
</div>
253+
</div>
254+
</div>
255+
);
256+
}
257+
258+
export default KeyboardTypingSpeedTesterPage;

0 commit comments

Comments
 (0)