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:
+22
-1
@@ -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
|
.env
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
@@ -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?
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import Code from './components/Code';
|
import Typing from 'pages/Typing';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Code />;
|
return <Typing />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
column-gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending, .space {
|
.pending, .space {
|
||||||
color: gray;
|
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 { KEYS_TO_DISABLE } from 'constants/default';
|
||||||
|
|
||||||
|
import { renderCharacter } from './utils';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
function Code() {
|
type CodeProps = {
|
||||||
// TODO: Cleanup the file and create utils for trimming the code properly.
|
code: string;
|
||||||
// TODO: Create a function that combines a sequence of spaces into tabs.
|
loaded: boolean;
|
||||||
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);
|
|
||||||
|
|
||||||
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());
|
function Code({code, loaded}: CodeProps) {
|
||||||
setLoaded(true);
|
const {
|
||||||
})();
|
startedTyping,
|
||||||
}, []);
|
characters,
|
||||||
|
timer,
|
||||||
|
|
||||||
useEffect(() => {
|
setStartedTyping,
|
||||||
let interval = null;
|
setCharacters,
|
||||||
|
setScore,
|
||||||
if (!startedTyping) {
|
setAccuracy,
|
||||||
if (interval) clearInterval(interval);
|
} = useTypingContext();
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
interval = setInterval(() => {
|
|
||||||
setTimer(prev => prev + 1);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [startedTyping]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyPress(event: KeyboardEvent) {
|
function handleKeyPress(event: KeyboardEvent) {
|
||||||
@@ -106,60 +73,12 @@ function Code() {
|
|||||||
setScore((typed / 5 - incorrectlyTyped) / (timer / 60));
|
setScore((typed / 5 - incorrectlyTyped) / (timer / 60));
|
||||||
setAccuracy(correctlyTyped / typed * 100);
|
setAccuracy(correctlyTyped / typed * 100);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [timer, characters]);
|
}, [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 (
|
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'>
|
<div className='code'>
|
||||||
{code.split('').map((char, index) => renderCharacter(char, index))}
|
{code.split('').map((char, index) => renderCharacter(code, characters, loaded, char, index))}
|
||||||
</div>
|
|
||||||
<div className='score'>
|
|
||||||
<p>Time: {timer}</p>
|
|
||||||
<p>WPM: {Math.round(score)}</p>
|
|
||||||
<p>Accuracy: {Math.round(accuracy)}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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() {
|
function Score() {
|
||||||
const [timer] = useState<number>(0);
|
const {
|
||||||
const [score] = useState<number>(0);
|
timer,
|
||||||
const [accuracy] = useState<number>(0);
|
score,
|
||||||
|
accuracy,
|
||||||
|
} = useTypingContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='score'>
|
<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;
|
startedTyping: boolean;
|
||||||
characters: Array<boolean | 'space'>;
|
characters: Array<boolean | 'space'>;
|
||||||
|
timer: number;
|
||||||
score: number;
|
score: number;
|
||||||
accuracy: number;
|
accuracy: number;
|
||||||
|
|
||||||
setStartedTyping: React.Dispatch<React.SetStateAction<boolean>>;
|
setStartedTyping: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setCharacters: React.Dispatch<React.SetStateAction<Array<boolean | 'space'>>>;
|
setCharacters: React.Dispatch<React.SetStateAction<Array<boolean | 'space'>>>;
|
||||||
|
setTimer: React.Dispatch<React.SetStateAction<number>>;
|
||||||
setScore: React.Dispatch<React.SetStateAction<number>>;
|
setScore: React.Dispatch<React.SetStateAction<number>>;
|
||||||
setAccuracy: React.Dispatch<React.SetStateAction<number>>;
|
setAccuracy: React.Dispatch<React.SetStateAction<number>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeContext = createContext<CodeContextValues>({
|
const TypingContext = createContext<TypingContextValues>({
|
||||||
startedTyping: false,
|
startedTyping: false,
|
||||||
characters: [],
|
characters: [],
|
||||||
|
timer: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
accuracy: 0,
|
accuracy: 0,
|
||||||
|
|
||||||
setStartedTyping: () => { },
|
setStartedTyping: () => { },
|
||||||
setCharacters: () => { },
|
setCharacters: () => { },
|
||||||
|
setTimer: () => { },
|
||||||
setScore: () => { },
|
setScore: () => { },
|
||||||
setAccuracy: () => { },
|
setAccuracy: () => { },
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useCodeContext() {
|
export function useTypingContext() {
|
||||||
return useContext(CodeContext);
|
return useContext(TypingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeContextProviderProps = {
|
type TypingContextProviderProps = {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodeContextProvider({ children }: CodeContextProviderProps) {
|
function TypingContextProvider({ children }: TypingContextProviderProps) {
|
||||||
const [startedTyping, setStartedTyping] = useState<boolean>(false);
|
const [startedTyping, setStartedTyping] = useState<boolean>(false);
|
||||||
const [characters, setCharacters] = useState<Array<boolean | 'space'>>([]);
|
const [characters, setCharacters] = useState<Array<boolean | 'space'>>([]);
|
||||||
|
const [timer, setTimer] = useState<number>(0);
|
||||||
const [score, setScore] = useState<number>(0);
|
const [score, setScore] = useState<number>(0);
|
||||||
const [accuracy, setAccuracy] = 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 (
|
return (
|
||||||
<CodeContext.Provider value={{
|
<TypingContext.Provider value={{
|
||||||
startedTyping,
|
startedTyping,
|
||||||
characters,
|
characters,
|
||||||
|
timer,
|
||||||
score,
|
score,
|
||||||
accuracy,
|
accuracy,
|
||||||
|
|
||||||
setStartedTyping,
|
setStartedTyping,
|
||||||
setCharacters,
|
setCharacters,
|
||||||
|
setTimer,
|
||||||
setScore,
|
setScore,
|
||||||
setAccuracy,
|
setAccuracy,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</CodeContext.Provider>
|
</TypingContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CodeContextProvider;
|
export default TypingContextProvider;
|
||||||
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App.jsx';
|
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
import './index.css';
|
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 />);
|
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