diff --git a/README.md b/README.md index e1a4173..6c6c76d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ An elegant typing test tool. https://github.com/gamer-ai/eletype-frontend/issues - ## Community Channel: Discord: ![Discord](https://img.shields.io/discord/993567075589181621?style=for-the-badge) @@ -41,6 +40,9 @@ Discord: ![Discord](https://img.shields.io/discord/993567075589181621?style=for- - KPM - Accuracy - Error analysis (correct/error/missing/extra chars count) + - Pacing Style (word pulse/ character caret): + - Pulse mode: the active word will have an underlien pulse, which helps improve the speed typing habit. + - Caret mode: a pacing caret, advancing character by character, which aligns normal typing habit. #### 2. Words Card (for English learners) diff --git a/package-lock.json b/package-lock.json index 62944a9..7408c26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eletype", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eletype", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", diff --git a/package.json b/package.json index e281b4b..9e9f685 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eletype", - "version": "0.3.0", + "version": "0.3.1", "private": true, "dependencies": { "@emotion/react": "^11.9.0", diff --git a/src/components/features/Keyboard/DefaultKeyboard.js b/src/components/features/Keyboard/DefaultKeyboard.js index de62d31..1c96bd5 100644 --- a/src/components/features/Keyboard/DefaultKeyboard.js +++ b/src/components/features/Keyboard/DefaultKeyboard.js @@ -9,15 +9,16 @@ const DefaultKeyboard = () => { const [inputChar, setInputChar] = useState(""); const [correctCount, setCorrectCount] = useState(0); const [incorrectCount, setIncorrectCount] = useState(0); - const accuracy = (correctCount + incorrectCount) === 0 ? 0 : Math.floor( - (correctCount / (correctCount + incorrectCount)) * 100 - ); + const accuracy = + correctCount + incorrectCount === 0 + ? 0 + : Math.floor((correctCount / (correctCount + incorrectCount)) * 100); const keys = [..." abcdefghijklmnopqrstuvwxyz "]; const resetStats = () => { setCorrectCount(0); setIncorrectCount(0); }; - + useEffect(() => { keyboardRef.current && keyboardRef.current.focus(); }); @@ -25,7 +26,6 @@ const DefaultKeyboard = () => { keyboardRef.current && keyboardRef.current.focus(); }; const handleKeyDown = (event) => { - setInputChar(event.key); event.preventDefault(); return; diff --git a/src/components/features/TypeBox/Stats.js b/src/components/features/TypeBox/Stats.js index 03f8bfa..10d1a81 100644 --- a/src/components/features/TypeBox/Stats.js +++ b/src/components/features/TypeBox/Stats.js @@ -3,46 +3,48 @@ import { Box } from "@mui/system"; import { Tooltip } from "@mui/material"; import { CHAR_TOOLTIP_TITLE } from "../../../constants/Constants"; -const Stats = ({ status, wpm, countDown, countDownConstant, statsCharCount, rawKeyStrokes}) => { +const Stats = ({ + status, + wpm, + countDown, + countDownConstant, + statsCharCount, + rawKeyStrokes, +}) => { return ( <> -

{countDown} s

- -

WPM: {Math.round(wpm)}

- {status === "finished" && ( -

Accuracy: {Math.round(statsCharCount[0])} %

- )} - {status === "finished" && ( - - {CHAR_TOOLTIP_TITLE} - - } - > -

- Char:{" "} - {statsCharCount[1]}/ - - {statsCharCount[2]} - - /{statsCharCount[3]} - /{statsCharCount[4]} - / - - {statsCharCount[5]} - -

-
- )} - {status === "finished" && ( +

{countDown} s

+ +

WPM: {Math.round(wpm)}

+ {status === "finished" && ( +

Accuracy: {Math.round(statsCharCount[0])} %

+ )} + {status === "finished" && ( + + {CHAR_TOOLTIP_TITLE} + + } + >

- Raw KPM: {Math.round((rawKeyStrokes / countDownConstant) * 60.0)} + Char:{" "} + {statsCharCount[1]}/ + {statsCharCount[2]}/ + {statsCharCount[3]}/ + {statsCharCount[4]}/ + {statsCharCount[5]}

- )} -
+ + )} + {status === "finished" && ( +

+ Raw KPM: {Math.round((rawKeyStrokes / countDownConstant) * 60.0)} +

+ )} +
); }; -export default Stats; \ No newline at end of file +export default Stats; diff --git a/src/components/features/TypeBox/TypeBox.js b/src/components/features/TypeBox/TypeBox.js index cd8904e..314120b 100644 --- a/src/components/features/TypeBox/TypeBox.js +++ b/src/components/features/TypeBox/TypeBox.js @@ -13,7 +13,6 @@ import CapsLockSnackbar from "../CapsLockSnackbar"; import Stats from "./Stats"; import { Dialog } from "@mui/material"; import DialogTitle from "@mui/material/DialogTitle"; - import { DEFAULT_COUNT_DOWN, COUNT_DOWN_60, @@ -30,7 +29,11 @@ import { CHINESE_MODE_TOOLTIP_TITLE, DEFAULT_DIFFICULTY_TOOLTIP_TITLE_CHINESE, HARD_DIFFICULTY_TOOLTIP_TITLE_CHINESE, - RESTART_BUTTON_TOOLTIP_TITLE + RESTART_BUTTON_TOOLTIP_TITLE, + PACING_CARET, + PACING_PULSE, + PACING_CARET_TOOLTIP, + PACING_PULSE_TOOLTIP, } from "../../../constants/Constants"; const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { @@ -40,6 +43,12 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { "timer-constant" ); + // local persist pacing style + const [pacingStyle, setPacingStyle] = useLocalPersistState( + PACING_PULSE, + "pacing-style" + ); + // local persist difficulty const [difficulty, setDifficulty] = useLocalPersistState( DEFAULT_DIFFICULTY, @@ -78,7 +87,7 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { return wordsGenerator(DEFAULT_WORDS_COUNT, difficulty, ENGLISH_MODE); } if (language === CHINESE_MODE) { - return chineseWordsGenerator(difficulty ,CHINESE_MODE); + return chineseWordsGenerator(difficulty, CHINESE_MODE); } }); @@ -133,14 +142,21 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { const [currChar, setCurrChar] = useState(""); useEffect(() => { - if (currWordIndex === DEFAULT_WORDS_COUNT - 1){ + if (currWordIndex === DEFAULT_WORDS_COUNT - 1) { if (language === ENGLISH_MODE) { - const generatedEng = wordsGenerator(DEFAULT_WORDS_COUNT, difficulty, ENGLISH_MODE); - setWordsDict(currentArray => [...currentArray, ...generatedEng]); + const generatedEng = wordsGenerator( + DEFAULT_WORDS_COUNT, + difficulty, + ENGLISH_MODE + ); + setWordsDict((currentArray) => [...currentArray, ...generatedEng]); } if (language === CHINESE_MODE) { - const generatedChinese = chineseWordsGenerator(difficulty ,CHINESE_MODE); - setWordsDict(currentArray => [...currentArray, ...generatedChinese]); + const generatedChinese = chineseWordsGenerator( + difficulty, + CHINESE_MODE + ); + setWordsDict((currentArray) => [...currentArray, ...generatedChinese]); } } if ( @@ -289,7 +305,7 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { } // disable shift alt ctrl - if (keyCode >= 16 && keyCode <= 18) { + if (keyCode >= 16 && keyCode <= 18) { e.preventDefault(); return; } @@ -369,6 +385,13 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { } }; + const getExtraCharClassName = (i, idx, extra) => { + if (currWordIndex === i && idx === extra.length - 1) { + return "caret-extra-char-right-error"; + } + return "error-char"; + }; + const getExtraCharsDisplay = (word, i) => { let input = inputWordsHistory[i]; if (!input) { @@ -383,7 +406,7 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { const extra = input.slice(word.length, input.length).split(""); history[i] = extra.length; return extra.map((c, idx) => ( - + {c} )); @@ -407,7 +430,7 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { // reset prevInput to empty (will not go back) setPrevInput(""); - // here count the space as effective wpm. + // here count the space as effective wpm. setWpmKeyStrokes(wpmKeyStrokes + 1); return true; } else { @@ -426,12 +449,20 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { const getWordClassName = (wordIdx) => { if (wordsInCorrect.has(wordIdx)) { if (currWordIndex === wordIdx) { - return "word error-word active-word"; + if (pacingStyle === PACING_PULSE) { + return "word error-word active-word"; + } else { + return "word error-word active-word-no-pulse"; + } } return "word error-word"; } else { if (currWordIndex === wordIdx) { - return "word active-word"; + if (pacingStyle === PACING_PULSE) { + return "word active-word"; + } else { + return "word active-word-no-pulse"; + } } return "word"; } @@ -454,22 +485,57 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { const getChineseWordClassName = (wordIdx) => { if (wordsInCorrect.has(wordIdx)) { if (currWordIndex === wordIdx) { - return "chinese-word error-word active-word"; + if (pacingStyle === PACING_PULSE) { + return "chinese-word error-word active-word"; + } else { + return "chinese-word error-word active-word-no-pulse"; + } } return "chinese-word error-word"; } else { if (currWordIndex === wordIdx) { - return "chinese-word active-word"; + if (pacingStyle === PACING_PULSE) { + return "chinese-word active-word"; + } else { + return "chinese-word active-word-no-pulse"; + } } return "chinese-word"; } }; - const getCharClassName = (wordIdx, charIdx, char) => { + + const getCharClassName = (wordIdx, charIdx, char, word) => { const keyString = wordIdx + "." + charIdx; + if ( + pacingStyle === PACING_CARET && + wordIdx === currWordIndex && + charIdx === currCharIndex + 1 && + status !== "finished" + ) { + return "caret-char-left"; + } if (history[keyString] === true) { + if ( + pacingStyle === PACING_CARET && + wordIdx === currWordIndex && + word.length - 1 === currCharIndex && + charIdx === currCharIndex && + status !== "finished" + ) { + return "caret-char-right-correct"; + } return "correct-char"; } if (history[keyString] === false) { + if ( + pacingStyle === PACING_CARET && + wordIdx === currWordIndex && + word.length - 1 === currCharIndex && + charIdx === currCharIndex && + status !== "finished" + ) { + return "caret-char-right-error"; + } return "error-char"; } if ( @@ -502,6 +568,13 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { return "inactive-button"; }; + const getPacingStyleButtonClassName = (buttonPacingStyle) => { + if (pacingStyle === buttonPacingStyle) { + return "active-button"; + } + return "inactive-button"; + }; + const getTimerButtonClassName = (buttonTimerCountDown) => { if (countDownConstant === buttonTimerCountDown) { return "active-button"; @@ -531,7 +604,7 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { {word.split("").map((char, idx) => ( {char} @@ -559,7 +632,7 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { {word.split("").map((char, idx) => ( {char} @@ -697,6 +770,36 @@ const TypeBox = ({ textInputRef, isFocusedMode, handleInputFocus }) => { )} + {menuEnabled && ( + + { + setPacingStyle(PACING_PULSE); + }} + > + + + {PACING_PULSE} + + + + { + setPacingStyle(PACING_CARET); + }} + > + + + {PACING_CARET} + + + + + )} diff --git a/src/constants/Constants.js b/src/constants/Constants.js index 61c0a91..56d9bcd 100644 --- a/src/constants/Constants.js +++ b/src/constants/Constants.js @@ -31,10 +31,10 @@ const SUPPORT_TOOLTIP_TITLE = const AUTHOR = "author: @Muyang Guo\n"; const GITHUB_REPO_LINK = "project: @Github\n"; -const FOCUS_MODE = "focus mode"; +const FOCUS_MODE = "Focus mode"; const MUSIC_MODE = - "spotify player. You will need to login spotify first to use the full feature."; + "Spotify player. You will need to login spotify first to use the full feature."; const FREE_MODE = "Free typing mode\nType any thing, no pressure, it's coffee time! \n "; @@ -55,7 +55,13 @@ const FIFTEEN_SENTENCES_COUNT = 15; const ENGLISH_SENTENCE_MODE_TOOLTIP_TITLE = "English Sentence Mode"; const CHINESE_SENTENCE_MODE_TOOLTIP_TITLE = "Chinese Sentence Mode"; -const WORDS_CARD_MODE = "Words Card Mode, learn something in typing!" +const WORDS_CARD_MODE = "Words Card mode, learn something in typing!" + +const PACING_CARET = "caret"; +const PACING_PULSE = "pulse"; + +const PACING_CARET_TOOLTIP = "type the word with a caret \"|\" , character by character."; +const PACING_PULSE_TOOLTIP = "type the word with a pulse \"____\", this helps improving wpm and your speed typing pace habit."; export { DEFAULT_WORDS_COUNT, @@ -97,5 +103,9 @@ export { WORDS_CARD_MODE, RESTART_BUTTON_TOOLTIP_TITLE_WORDSCARD, SELECT_ONE_OR_MORE_CHAPTERS, - RECITE_MODE_TITLE + RECITE_MODE_TITLE, + PACING_CARET, + PACING_PULSE, + PACING_CARET_TOOLTIP, + PACING_PULSE_TOOLTIP }; diff --git a/src/style/global.js b/src/style/global.js index 2e7dfad..7fef4b4 100644 --- a/src/style/global.js +++ b/src/style/global.js @@ -178,29 +178,75 @@ export const GlobalStyles = createGlobalStyle` } .active-word{ animation: blinkingBackground 2s infinite; + border-top: 1px solid transparent; border-bottom: 1px solid; @keyframes blinkingBackground{ 0% { border-bottom-color: ${({ theme }) => theme.stats};} 25% { border-bottom-color: ${({ theme }) => theme.textTypeBox};} 50% { border-bottom-color: ${({ theme }) => theme.stats};} 75% {border-bottom-color: ${({ theme }) => theme.textTypeBox};} - 100% {border-bottom-color: ${({ theme }) => theme.stats};} - } + 100% {border-bottom-color: ${({ theme }) => theme.stats};} + }; + scroll-margin: 4px; + } + .active-word-no-pulse{ + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + scroll-margin: 4px; } .error-word{ border-bottom: 1px solid red; scroll-margin: 4px; } .char{ - padding-right: 1px; + border-left: 1px solid transparent; + border-right: 1px solid transparent; } .correct-char{ + border-left: 1px solid transparent; + border-right: 1px solid transparent; color: ${({ theme }) => theme.text}; - padding-right: 1px; } .error-char{ + border-left: 1px solid transparent; + border-right: 1px solid transparent; + color: red; + } + .caret-char-left{ + border-left: 1px solid ${({ theme }) => theme.stats}; + border-right: 1px solid transparent; + } + .caret-char-left-start{ + border-left: 1px solid; + border-right: 1px solid transparent; + animation: blinkingCaretLeft 2s infinite; + animation-timing-function: ease; + @keyframes blinkingCaretLeft{ + 0% { border-left-color: ${({ theme }) => theme.stats};} + 25% { border-left-color: ${({ theme }) => theme.textTypeBox};} + 50% { border-left-color: ${({ theme }) => theme.stats};} + 75% { border-left-color: ${({ theme }) => theme.textTypeBox};} + 100% { border-left-color: ${({ theme }) => theme.stats};} + } + } + .caret-char-right{ + border-right: 1px solid ${({ theme }) => theme.stats}; + border-left: 1x solid transparent; + } + .caret-char-right-correct{ + color: ${({ theme }) => theme.text}; + border-right: 1px solid ${({ theme }) => theme.stats}; + border-left: 1px solid transparent; + } + .caret-char-right-error{ + color: red; + border-right: 1px solid ${({ theme }) => theme.stats}; + border-left: 1px solid transparent; + } + .caret-extra-char-right-error{ color: red; - padding-right: 1px; + border-right: 1px solid ${({ theme }) => theme.stats}; + border-left: 1px solid transparent; } .hidden-input{