-
- {code.split('').map((char, index) => renderCharacter(char, index))}
-
-
-
Time: {timer}
-
WPM: {Math.round(score)}
-
Accuracy: {Math.round(accuracy)}%
-
+
+ {code.split('').map((char, index) => renderCharacter(code, characters, loaded, char, index))}
);
}
diff --git a/client/src/components/Code/utils.tsx b/client/src/components/Code/utils.tsx
new file mode 100644
index 0000000..4e939d5
--- /dev/null
+++ b/client/src/components/Code/utils.tsx
@@ -0,0 +1,57 @@
+function renderCharacterClassName(
+ code: string,
+ characters: Array
,
+ 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,
+ loaded: boolean,
+ index: number,
+) {
+ return `${renderCharacterClassName(code, characters, loaded, index)}`.trim();
+}
+
+export function renderCharacter(
+ code: string,
+ characters: Array,
+ loaded: boolean,
+ char: string,
+ index: number,
+) {
+ const rendered = renderSpacingCharacter(char);
+
+ if (/^\n$/.test(rendered)) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {rendered}
+
+ );
+}
diff --git a/client/src/components/Stats/index.css b/client/src/components/Stats/index.css
new file mode 100644
index 0000000..65c3a4c
--- /dev/null
+++ b/client/src/components/Stats/index.css
@@ -0,0 +1,3 @@
+.score {
+ text-align: right;
+}
diff --git a/client/src/components/Stats/index.tsx b/client/src/components/Stats/index.tsx
index 7f7c610..5ac999e 100644
--- a/client/src/components/Stats/index.tsx
+++ b/client/src/components/Stats/index.tsx
@@ -1,9 +1,13 @@
-import { useState } from "react";
+import { useTypingContext } from "contexts/typing";
+
+import './index.css';
function Score() {
- const [timer] = useState(0);
- const [score] = useState(0);
- const [accuracy] = useState(0);
+ const {
+ timer,
+ score,
+ accuracy,
+ } = useTypingContext();
return (
diff --git a/client/src/contexts/code.tsx b/client/src/contexts/typing.tsx
similarity index 52%
rename from client/src/contexts/code.tsx
rename to client/src/contexts/typing.tsx
index 8c03b81..d5bc820 100644
--- a/client/src/contexts/code.tsx
+++ b/client/src/contexts/typing.tsx
@@ -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
;
+ timer: number;
score: number;
accuracy: number;
setStartedTyping: React.Dispatch>;
setCharacters: React.Dispatch>>;
+ setTimer: React.Dispatch>;
setScore: React.Dispatch>;
setAccuracy: React.Dispatch>;
}
-const CodeContext = createContext({
+const TypingContext = createContext({
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(false);
const [characters, setCharacters] = useState>([]);
+ const [timer, setTimer] = useState(0);
const [score, setScore] = useState(0);
const [accuracy, setAccuracy] = useState(0);
+ useEffect(() => {
+ let interval = null;
+
+ if (!startedTyping) {
+ if (interval) clearInterval(interval);
+
+ return;
+ }
+
+ interval = setInterval(() => {
+ setTimer(prev => prev + 1);
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, [startedTyping]);
+
return (
-
{children}
-
+
)
}
-export default CodeContextProvider;
+export default TypingContextProvider;
diff --git a/client/src/main.tsx b/client/src/main.tsx
index de57103..bb67a0e 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -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();
diff --git a/client/src/pages/Typing/index.css b/client/src/pages/Typing/index.css
new file mode 100644
index 0000000..2a8c521
--- /dev/null
+++ b/client/src/pages/Typing/index.css
@@ -0,0 +1,5 @@
+.container {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ column-gap: 1rem;
+}
diff --git a/client/src/pages/Typing/index.tsx b/client/src/pages/Typing/index.tsx
new file mode 100644
index 0000000..1c0886d
--- /dev/null
+++ b/client/src/pages/Typing/index.tsx
@@ -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('');
+ const [loaded, setLoaded] = useState(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 (
+
+
+
+
+
+
+ );
+}
+
+export default Typing;