Typing stats and code cleanup and organization

This commit is contained in:
2025-02-15 19:06:19 +01:00
parent 4fc0fbf1ed
commit 4bb441a125
13 changed files with 209 additions and 157 deletions
+22 -1
View File
@@ -1,3 +1,24 @@
/node_modules
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.env
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-24
View File
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
View File
+2 -2
View File
@@ -1,7 +1,7 @@
import Code from './components/Code';
import Typing from 'pages/Typing';
function App() {
return <Code />;
return <Typing />;
}
export default App;
-10
View File
@@ -1,17 +1,7 @@
.container {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 1rem;
}
.code {
padding: 1rem;
}
.score {
text-align: right;
}
.pending, .space {
color: gray;
}
+22 -103
View File
@@ -1,62 +1,29 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useTypingContext } from 'contexts/typing';
import { KEYS_TO_DISABLE } from 'constants/default';
import { renderCharacter } from './utils';
import './index.css';
function Code() {
// TODO: Cleanup the file and create utils for trimming the code properly.
// TODO: Create a function that combines a sequence of spaces into tabs.
const [code, setCode] = useState<string>('');
const [loaded, setLoaded] = useState<boolean>(false);
const [startedTyping, setStartedTyping] = useState<boolean>(false);
const [characters, setCharacters] = useState<Array<boolean | 'space'>>([]);
const [timer, setTimer] = useState<number>(0);
const [score, setScore] = useState<number>(0);
const [accuracy, setAccuracy] = useState<number>(0);
type CodeProps = {
code: string;
loaded: boolean;
}
useEffect(() => {
(async function () {
setCode('');
function Code({code, loaded}: CodeProps) {
const {
startedTyping,
characters,
timer,
const response = await fetch(
`${import.meta.env.VITE_API_URL}/generate?lang=lisp`,
);
if (!response.ok || !response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
setCode((prev) => prev + decoder.decode(value));
if (done) {
break;
}
}
setCode((prev) => prev.trim());
setLoaded(true);
})();
}, []);
useEffect(() => {
let interval = null;
if (!startedTyping) {
if (interval) clearInterval(interval);
return;
}
interval = setInterval(() => {
setTimer(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [startedTyping]);
setStartedTyping,
setCharacters,
setScore,
setAccuracy,
} = useTypingContext();
useEffect(() => {
function handleKeyPress(event: KeyboardEvent) {
@@ -106,60 +73,12 @@ function Code() {
setScore((typed / 5 - incorrectlyTyped) / (timer / 60));
setAccuracy(correctlyTyped / typed * 100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timer, characters]);
function renderCharacterClassName(index: number) {
const typed = characters[index];
if (loaded && index === characters.length) return 'highlight';
if (typeof typed === 'undefined') return 'pending';
if (typed === 'space') return 'space';
if (!typed && /^\n$/.test(code[index])) return 'incorrect-enter';
if (!typed && /^\s$/.test(code[index])) return 'incorrect-space';
if (!typed) return 'incorrect';
return 'correct';
}
function renderSpacingCharacter(char: string) {
if (/^\n$/.test(char)) return '\n';
if (/^(\s|\t)$/.test(char)) return '\u00A0';
return char;
}
function renderClassName(index: number) {
return `${renderCharacterClassName(index)}`.trim();
}
function renderCharacter(char: string, index: number) {
const rendered = renderSpacingCharacter(char);
if (/^\n$/.test(rendered)) {
return (
<span className={renderClassName(index)} key={char + index}>
<br />
</span>
);
}
return (
<span className={renderClassName(index)} key={char + index}>
{rendered}
</span>
);
}
return (
<div className='container'>
<div className='code'>
{code.split('').map((char, index) => renderCharacter(char, index))}
</div>
<div className='score'>
<p>Time: {timer}</p>
<p>WPM: {Math.round(score)}</p>
<p>Accuracy: {Math.round(accuracy)}%</p>
</div>
{code.split('').map((char, index) => renderCharacter(code, characters, loaded, char, index))}
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
function renderCharacterClassName(
code: string,
characters: Array<boolean | 'space'>,
loaded: boolean,
index: number,
) {
const typed = characters[index];
if (loaded && index === characters.length) return 'highlight';
if (typeof typed === 'undefined') return 'pending';
if (typed === 'space') return 'space';
if (!typed && /^\n$/.test(code[index])) return 'incorrect-enter';
if (!typed && /^\s$/.test(code[index])) return 'incorrect-space';
if (!typed) return 'incorrect';
return 'correct';
}
function renderSpacingCharacter(char: string) {
if (/^\n$/.test(char)) return '\n';
if (/^(\s|\t)$/.test(char)) return '\u00A0';
return char;
}
function renderClassName(
code: string,
characters: Array<boolean | 'space'>,
loaded: boolean,
index: number,
) {
return `${renderCharacterClassName(code, characters, loaded, index)}`.trim();
}
export function renderCharacter(
code: string,
characters: Array<boolean | 'space'>,
loaded: boolean,
char: string,
index: number,
) {
const rendered = renderSpacingCharacter(char);
if (/^\n$/.test(rendered)) {
return (
<span className={renderClassName(code, characters, loaded, index)} key={char + index}>
<br />
</span>
);
}
return (
<span className={renderClassName(code, characters, loaded, index)} key={char + index}>
{rendered}
</span>
);
}
+3
View File
@@ -0,0 +1,3 @@
.score {
text-align: right;
}
+8 -4
View File
@@ -1,9 +1,13 @@
import { useState } from "react";
import { useTypingContext } from "contexts/typing";
import './index.css';
function Score() {
const [timer] = useState<number>(0);
const [score] = useState<number>(0);
const [accuracy] = useState<number>(0);
const {
timer,
score,
accuracy,
} = useTypingContext();
return (
<div className='score'>
@@ -1,58 +1,81 @@
import { createContext, useContext, useState } from "react";
import { createContext, useContext, useEffect, useState } from "react";
export type CodeContextValues = {
export type TypingContextValues = {
startedTyping: boolean;
characters: Array<boolean | 'space'>;
timer: number;
score: number;
accuracy: number;
setStartedTyping: React.Dispatch<React.SetStateAction<boolean>>;
setCharacters: React.Dispatch<React.SetStateAction<Array<boolean | 'space'>>>;
setTimer: React.Dispatch<React.SetStateAction<number>>;
setScore: React.Dispatch<React.SetStateAction<number>>;
setAccuracy: React.Dispatch<React.SetStateAction<number>>;
}
const CodeContext = createContext<CodeContextValues>({
const TypingContext = createContext<TypingContextValues>({
startedTyping: false,
characters: [],
timer: 0,
score: 0,
accuracy: 0,
setStartedTyping: () => { },
setCharacters: () => { },
setTimer: () => { },
setScore: () => { },
setAccuracy: () => { },
});
export function useCodeContext() {
return useContext(CodeContext);
export function useTypingContext() {
return useContext(TypingContext);
}
type CodeContextProviderProps = {
type TypingContextProviderProps = {
children: React.ReactNode,
}
function CodeContextProvider({ children }: CodeContextProviderProps) {
function TypingContextProvider({ children }: TypingContextProviderProps) {
const [startedTyping, setStartedTyping] = useState<boolean>(false);
const [characters, setCharacters] = useState<Array<boolean | 'space'>>([]);
const [timer, setTimer] = useState<number>(0);
const [score, setScore] = useState<number>(0);
const [accuracy, setAccuracy] = useState<number>(0);
useEffect(() => {
let interval = null;
if (!startedTyping) {
if (interval) clearInterval(interval);
return;
}
interval = setInterval(() => {
setTimer(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [startedTyping]);
return (
<CodeContext.Provider value={{
<TypingContext.Provider value={{
startedTyping,
characters,
timer,
score,
accuracy,
setStartedTyping,
setCharacters,
setTimer,
setScore,
setAccuracy,
}}>
{children}
</CodeContext.Provider>
</TypingContext.Provider>
)
}
export default CodeContextProvider;
export default TypingContextProvider;
+4 -2
View File
@@ -1,6 +1,8 @@
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import App from './App.tsx';
import './index.css';
// TODO: Bring back string mode when building and deploying
// TODO: Bring back strict mode when building and deploying
createRoot(document.getElementById('root')).render(<App />);
+5
View File
@@ -0,0 +1,5 @@
.container {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 1rem;
}
+52
View File
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';
import TypingContextProvider from 'contexts/typing';
import Code from 'components/Code';
import Stats from 'components/Stats';
import './index.css';
function Typing() {
const [code, setCode] = useState<string>('');
const [loaded, setLoaded] = useState<boolean>(false);
useEffect(() => {
(async function () {
setCode('');
const response = await fetch(
`${import.meta.env.VITE_API_URL}/generate?lang=lisp`,
);
if (!response.ok || !response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
setCode((prev) => prev + decoder.decode(value));
if (done) {
break;
}
}
setCode((prev) => prev.trim());
setLoaded(true);
})();
}, []);
return (
<TypingContextProvider>
<div className='container'>
<Code code={code} loaded={loaded} />
<Stats />
</div>
</TypingContextProvider>
);
}
export default Typing;