diff --git a/packages/web/src/components/common/typingBox/helpers/interfaces.ts b/packages/web/src/components/common/typingBox/helpers/interfaces.ts new file mode 100644 index 0000000..303b293 --- /dev/null +++ b/packages/web/src/components/common/typingBox/helpers/interfaces.ts @@ -0,0 +1,20 @@ +export interface typingBoxProps { + mode: "easy" | "hard"; + colorCodes: { + wrong: string; + correct: string; + notTyped: string; + }; +} +export interface typedArrayInterface { + word: string; + state: "correct" | "wrong"; + time: number; + wpm: number; + accuracy: number; + timeUsed: string; +} + +export interface DataBoxType { + data: Array; +} diff --git a/packages/web/src/components/common/typingBox/style.tsx b/packages/web/src/components/common/typingBox/style.tsx index c979c9e..eb0f92b 100644 --- a/packages/web/src/components/common/typingBox/style.tsx +++ b/packages/web/src/components/common/typingBox/style.tsx @@ -1,37 +1,64 @@ -import styled from "@emotion/styled" +import styled from "@emotion/styled"; export const Wrapper = styled.div` width: 100%; -` -export const LetterElement = styled.span` - display: inline; - font-size: 23px; -` + padding: 15px; +`; export const Container = styled.div` - @import url("https://fonts.googleapis.com/css?family=Hind&display=swap"); - font-family: "Hind"; - width: 750px; height: 250px; position: relative; + text-align: center; margin: auto; -` -export const Top = styled.div` + @import url("https://fonts.googleapis.com/css?family=Hind&display=swap"); + font-family: "Hind"; +`; + +export const Displayer = styled.div` text-align: center; width: 100%; height: 25px; border-bottom: 1px solid #9e9e9e; -` +`; + +export const TextBox = styled.div` + margin-left: 2px; + display: inline-flex; + flex-wrap: wrap; + text-align: center; + justify-content: center; + font-size: 20px; -export const Bottom = styled.input` + overflow-y: scroll; + scroll-behavior: smooth; + + -ms-overflow-style: none; + + width: 99%; + max-height: 200px; + + &::-webkit-scrollbar { + display: none; + } + .spaced { + padding-right: 4px; + } + .isBeingTyped { + display: inline-flex; + flex-wrap: wrap; + } +`; + +export const InputBox = styled.input` width: 100%; border-style: solid; border-width: 0; border-top: 1px solid #9e9e9e; + border-bottom: 1px solid #9e9e9e; position: absolute; bottom: 0; @@ -42,22 +69,22 @@ export const Bottom = styled.input` outline: none; - font-size: 24px; -` -export const Text = styled.div` - margin-left: 2px; - - text-align: center; - - overflow: scroll; - scroll-behavior: smooth; - - -ms-overflow-style: none; + font-size: 22px; +`; - width: 99%; - height: 200px; +export const DataBoxWrapper = styled.div` + width: 100%; + border-bottom: 1px solid #9e9e9e; +`; - &::-webkit-scrollbar { - display: none; - } -` +export const TryAgainButton = styled.button` + background: #198cf6; + border: none; + text-align: center; + text-decoration: none; + color: white; + font-size: 18px; + font-weight: bold; + margin-top: 10px; + padding: 3px; +`; diff --git a/packages/web/src/components/common/typingBox/typingBox.tsx b/packages/web/src/components/common/typingBox/typingBox.tsx index 4af220f..86f7ba4 100644 --- a/packages/web/src/components/common/typingBox/typingBox.tsx +++ b/packages/web/src/components/common/typingBox/typingBox.tsx @@ -1,271 +1,209 @@ -import React, { useState, useEffect } from "react"; - -import { Wrapper, Container, Top, Text, Bottom, LetterElement } from "./style"; +import React, { useState, useEffect, useRef } from "react"; + +import { getText } from "./helpers/gettext"; +import { typingBoxProps, typedArrayInterface } from "./helpers/interfaces"; +import { DataBox } from "./dataBox"; + +import { + Wrapper, + Container, + Displayer, + TextBox, + InputBox, + TryAgainButton +} from "./style"; + +export const TypingBox = (props: typingBoxProps) => { + const [input, setInput] = useState(""); + const [text, setText] = useState(getText(props.mode)); + const [visibleText, setVisibleText] = useState(text.join(" ")); + const [typed, setTyped] = useState([]); + const [time, setTime] = useState(60); + const [cpm, setCpm] = useState(0); -let string = ""; -let increment = 0; -let color = "green"; -let stringFullyBackspaced = false; -let charTyped = 0; -let correctedCharTyped = 0; -let CPM = 0; -let WPM = 0; + const textBoxRef = useRef(null); -const getText = () => { - return "A (from the Ancient Greek paragraphos, \"to write beside\" or \"written beside\") is a self-contained unit of a discourse in writing with a particular point or idea. A paragraph consists of one or more sentences. Though not required by the syntax of any language, paragraphs are usually an expected part of formal writing, used to organize longer prose."; -}; + useEffect(() => { + setVisibleText(generateVisibleText(input, props.mode, typed)); + }, []); + + const getCpm = ( + array: Array, + time: number + ): number => { + const charTyped = array + .map((value: typedArrayInterface) => { + return value.state === "correct" ? value.word.length : 0; + }) + .reduce((prev: number, currentValue: number) => { + return prev + currentValue; + }); + + return Math.floor(charTyped / ((60 - time === 0 ? 1 : 60 - time) / 60)); + }; -const checkBackspaceReturn = (e: any) => { - onKey( - 0, - e.keyCode === 8 - ? "backspace" - : e.keyCode === 13 - ? "enter" - : e.keyCode === 32 - ? "space" - : "undefined" - ); -}; -const onKey = (e: any, special: string = "undefined") => { - if ( - window.event && - e != 0 && - String.fromCharCode(e.charCode) != " " && - String.fromCharCode(e.charCode) != "" - ) { - string += String.fromCharCode(e.charCode); - stringFullyBackspaced = false; - } else if (special == "backspace") { - string = string.slice(0, -1); - if (string.length === 0) { - stringFullyBackspaced = true; + const getAccuracy = (array: Array): number => { + let numberOfWrongWords = 0; + let numberOfCorrectWords = 0; + for (let value of array) { + value.state === "correct" + ? numberOfCorrectWords++ + : numberOfWrongWords++; } - } else if (special == "enter") { - checkAndIncrement(); - } else if (special == "space") { - checkAndIncrement(); - } - return string; -}; - -const buildTextComponentsArray = (text: string, elColor: string) => { - const splittedText = text.split(" "); - return splittedText.map((word: string, index) => { - if (index <= increment) { - if (index === increment) { - return splittedText[increment] - .split("") - .map((letter: string, i) => { - let addSpace = ""; - if (word.split("").length - 1 === i) { - addSpace = " "; - } - - if ( - string.split("")[i] === letter && - !stringFullyBackspaced - ) { - return ( - word.length - ? "#e83c3c" - : elColor - }} - > - {letter} - {addSpace} - - ); - } else { - if ( - i <= string.split("").length - 1 && - !stringFullyBackspaced - ) { - return ( - - {letter} - {addSpace} - - ); - } else { - if (stringFullyBackspaced) { - stringFullyBackspaced = false; - } - - return ( - - {letter} - {addSpace} - - ); - } - } - }); - } else { - return ( - - {word}{" "} - - ); - } - } return ( - - {word}{" "} - - ); - }); -}; - -const getTextToBeCompared = (text: string) => { - return text.split(" "); -}; - -const checkAndIncrement = () => { - if (string === getTextToBeCompared(getText())[increment]) { - increment++; - charTyped += string.length + 1; - correctedCharTyped += string.length + 1; - color = "green"; - string = ""; - } else { - correctedCharTyped += string.length + 1; - console.log( - string, - increment, - getTextToBeCompared(getText())[increment] + Math.floor( + (numberOfCorrectWords / + (array.length != 0 ? array.length : 1)) * + 10000 + ) / 100 ); - } -}; - -const setCpmWpm = (cpm: number, wpm: number) => { - CPM = cpm; - WPM = wpm; -}; - -const Input = () => { - const [inner, changeInner] = useState(""); - - return ( - changeInner(onKey(e))} - onKeyDown={e => { - checkBackspaceReturn(e); - changeInner(string); - }} - onChange={() => {}} - autoFocus - > - ); -}; - -export const Box = () => { - const [text, updateText] = useState( - buildTextComponentsArray(getText(), color) - ); - const [time, setTime] = useState(60); - const [cpm, setCpm] = useState(0); - const [wpm, setWpm] = useState(0); - - if (time === 0) { - setCpmWpm(cpm, wpm); - } else if (time === 60 && cpm === 0 && CPM === 0) { - setCpmWpm(-1, 0); - } - const calculateCpmWpm = () => { - return time === 0 - ? [CPM, WPM] - : [ - Math.floor( - charTyped / ((60 - time === 0 ? 1 : 60 - time) / 60) - ), - Math.floor( - charTyped / 5 / ((60 - time === 0 ? 1 : 60 - time) / 60) - ) - ]; - }; - const resetEverything = () => { - string = ""; - increment = 0; - color = "green"; - stringFullyBackspaced = false; - charTyped = 0; - correctedCharTyped = 0; - CPM = 0; - WPM = 0; }; - - useEffect(() => { - setTimeout(() => { - setTime( - time === 0 || increment >= getTextToBeCompared(getText()).length - ? 0 - : CPM === -1 && cpm === 0 - ? 60 - : time - 1 + const generateVisibleText = ( + input: string, + mode: string, + typedArray: Array + ) => { + return text.map((value: string, index: number) => { + return index < typedArray.length ? ( +
+ {text[index]} +
+ ) : index === typedArray.length ? ( +
+ {text[index].split("").map((v: string, i: number) => { + return ( +
+ {v} +
+ ); + })} +
+ ) : ( +
+ {text[index]} +
); - setCpm(calculateCpmWpm()[0]); - setWpm(calculateCpmWpm()[1]); - }, 1000); - }); + }); + }; return ( - { - updateText(buildTextComponentsArray(getText(), color)); - }} - onKeyPress={() => { - updateText(buildTextComponentsArray(getText(), color)); - }} - > - - {"Time left: "} - {time} - {" "} - {"CPM:"} - {cpm} - {" "} - {"WPM:"} - {wpm} -
+ + CPM: {cpm} WPM: {Math.floor(cpm / 5)} Time: {time} + + {time > 0 ? "" : } + {time > 0 ? ( + "" + ) : ( + { + history.go(0); }} > - -
-
- {text} - + Try again + + )} + 0 ? "" : "none" }} + ref={textBoxRef} + > + {visibleText} + + 0 ? "" : "none" }} + readOnly={!(time > 0)} + autoFocus + value={input} + onChange={(e: React.FormEvent) => { + const timeLeft = typed.length + ? 60 - + Math.floor( + performance.now() / 1000 - typed[0].time + ) > + 0 + ? 60 - + Math.floor( + performance.now() / 1000 - typed[0].time + ) + : 0 + : 60; + + const CPM = typed.length + ? getCpm(typed, timeLeft) + : cpm; + + const typedArray: Array = + e.target.value[e.target.value.length - 1] === " " || + time <= 0 + ? [ + ...typed, + { + word: e.target.value.replace( + / +/g, + "" + ), + state: + e.target.value.replace( + / +/g, + "" + ) === text[typed.length] + ? "correct" + : "wrong", + time: performance.now() / 1000, + wpm: CPM / 5, + accuracy: getAccuracy(typed), + timeUsed: 60 - timeLeft + "s" + } + ] + : typed; + const input = + e.target.value[e.target.value.length - 1] === " " + ? "" + : e.target.value; + + setInput(input); + setTyped(typedArray); + setVisibleText( + generateVisibleText(input, props.mode, typedArray) + ); + setTime(timeLeft); + setCpm(CPM); + + const elm = document.getElementById("isBeingTyped"); + if (elm) { + textBoxRef.current.scrollTop = elm.offsetTop - 60; + } + }} + >
); diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx index edbaea5..4146b11 100644 --- a/packages/web/src/index.tsx +++ b/packages/web/src/index.tsx @@ -9,8 +9,7 @@ import { Footer } from "./components/common/footer/footer"; import { Global } from "@emotion/core"; import { globalStyle, Container, Content } from "./style"; -import { Box } from "./components/common/typingBox/typingBox"; -import { TypingBox } from "./components/common/typingBox/yetanothertypingBox"; +import { TypingBox } from "./components/common/typingBox/typingBox"; const App = () => { return ( @@ -25,9 +24,6 @@ const App = () => { - - -