mirror of
https://github.com/hazemKrimi/touch-programming.git
synced 2026-05-01 18:20:26 +00:00
Typing stats and code cleanup and organization
This commit is contained in:
+2
-2
@@ -1,7 +1,7 @@
|
||||
import Code from './components/Code';
|
||||
import Typing from 'pages/Typing';
|
||||
|
||||
function App() {
|
||||
return <Code />;
|
||||
return <Typing />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<div className='code'>
|
||||
{code.split('').map((char, index) => renderCharacter(code, characters, loaded, char, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.score {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -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
@@ -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 />);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: 1rem;
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user