diff --git a/package.json b/package.json index 6fea90829acf3ccb3569d738bb240abb65f913df..b8dac5dae6dcc33cde540638e1c2ecbb82b00bca 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "ramda": "^0.28.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", diff --git a/src/components/CharBox.js b/src/components/CharBox.js index dbf5f1ce6ec5a228f09890fd1eab0f12e6bcb4d6..d92c1e80a6dbc012f40438db3602973b69c043e4 100644 --- a/src/components/CharBox.js +++ b/src/components/CharBox.js @@ -1,83 +1,10 @@ +import './style/Boxes.css'; import React from "react"; -// import { Answer } from "../constant/Answer"; export default function CharBox(props) { - - let correctBox = { - backgroundColor: "green", - color: "white", - width: "50px", - height: "50px", - display: "inline-block", - textAlign: "center", - verticalAlign: "middle", - lineHeight: "50px", - margin: "5px", - border: "1px solid black" - }; - - let missplacedBox = { - backgroundColor: "yellow", - color: "black", - width: "50px", - height: "50px", - display: "inline-block", - textAlign: "center", - verticalAlign: "middle", - lineHeight: "50px", - margin: "5px", - border: "1px solid black" - }; - - let wrongBox = { - backgroundColor: "red", - color: "white", - width: "50px", - height: "50px", - display: "inline-block", - textAlign: "center", - verticalAlign: "middle", - lineHeight: "50px", - margin: "5px", - border: "1px solid black" - }; - - let defaultBox = { - backgroundColor: "white", - color: "black", - width: "50px", - height: "50px", - display: "inline-block", - textAlign: "center", - verticalAlign: "middle", - lineHeight: "50px", - margin: "5px", - border: "1px solid black" - }; - - const [charState, setCharState] = React.useState(props.char); - const [boxStyle, setBoxStyle] = React.useState(defaultBox); - - const changeBoxStyle = (answer) => { - if (answer === "Correct") { - return correctBox; - } else if (answer === "Misplaced") { - return missplacedBox; - } else if (answer === "Wrong") { - return wrongBox; - } - return defaultBox; - } - - React.useEffect(() => { - // console.log(props); - setCharState(props.char); - setBoxStyle(changeBoxStyle(props.answer)); - }, [props]); // eslint-disable-line react-hooks/exhaustive-deps - return ( - <div style={boxStyle}> - {charState.toUpperCase()} + <div class={"char-box " + (props.answer ? props.answer + "-box" : "default-box")}> + {props.char.toUpperCase()} </div> ); } \ No newline at end of file diff --git a/src/components/WordBox.js b/src/components/WordBox.js index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..86578b816077d8dbad1ff7e10b67c65fddff2655 100644 --- a/src/components/WordBox.js +++ b/src/components/WordBox.js @@ -0,0 +1,16 @@ +import React from "react"; +import CharBox from "./CharBox"; + +const WordBox = (props) => { + return ( + <div className="word-box"> + <CharBox char={props.chars[0].char} answer={props.chars[0].answer}/> + <CharBox char={props.chars[1].char} answer={props.chars[1].answer}/> + <CharBox char={props.chars[2].char} answer={props.chars[2].answer}/> + <CharBox char={props.chars[3].char} answer={props.chars[3].answer}/> + <CharBox char={props.chars[4].char} answer={props.chars[4].answer}/> + </div> + ); +} + +export default WordBox; \ No newline at end of file diff --git a/src/components/WordInput.js b/src/components/WordInput.js index ca15a6ca03f22887255c3f28c6b65912b5f52d87..e84f7b4a3b87b02ba6288d136dcc470a290ffcd8 100644 --- a/src/components/WordInput.js +++ b/src/components/WordInput.js @@ -1,44 +1,79 @@ import React from "react"; import { validateInput } from "../helpers/validateInput"; import CharBox from "./CharBox"; +import WordBox from "./WordBox"; const WordInput = () => { + const NUM_TRY = 6; + const WORD_LENGTH = 5; + const [currentTry, setCurrentTry] = React.useState(0); const [word, setWord] = React.useState([]); - const [charBox, setCharBox] = React.useState([]); + const [allWords, setAllWords] = React.useState([]); + const [inputBox, setInputBox] = React.useState([]); + + const resetInputBox = () => { + const initInputBox = []; + for (let i = 0; i < WORD_LENGTH; i++) { + initInputBox.push(<CharBox char=""/>); + } + setInputBox(initInputBox); + setWord([]); + } React.useEffect(() => { const charBoxes = []; - for (let i = 0; i < 5; i++) { - charBoxes.push(<CharBox char=""/>); + for (let i = 0; i < WORD_LENGTH; i++) { + charBoxes.push({ char: "", answer: "" }); + } + for (let i = 0; i < NUM_TRY; i++) { + setAllWords(prev => [...prev, <WordBox chars={charBoxes} />]); } - setCharBox(charBoxes); + resetInputBox(); }, []); - document.onkeyup = function(e) { + document.onkeyup = async function(e) { if (e.key.length === 1){ if (e.key.match(/[a-z]/i)) { - if (word.length >= 5) { + if (word.length >= WORD_LENGTH) { return; } else { - setCharBox(prevState => { + setInputBox(prevState => { const newState = [...prevState]; newState[word.length] = <CharBox char={e.key} />; return newState; - }); - setWord([...word, e.key]); } + }); + setWord([...word, e.key]); + } } } else { if (e.key === "Enter") { - setCharBox([]); - - validateInput(word, "hello").flatMap((e, idx) => { - setCharBox(prevState => [...prevState, <CharBox char={word[idx]} answer={e} />]); - return charBox; - }); + // TODO: Check if user win + // TODO: Check if user lose + // TODO: Fix bug for character less than asked + // TODO: Call word (answer) randomizer API + // TODO: Handle error to be served on Pop-up/Dialog box + + let currentInput = []; + const validatedInput = await validateInput(word, "kapal"); + if (validatedInput["error"]) { + console.log(validatedInput["error"]); + } else { + validatedInput["result"].flatMap((e, idx) => { + currentInput = [...currentInput, { char: word[idx], answer: e }]; + }); + setAllWords(prev => { + const newState = [...prev]; + newState[currentTry] = <WordBox chars={currentInput} />; + return newState; + }); + setCurrentTry(prev => prev + 1); + resetInputBox(); + } + } else if (e.key === "Backspace" && word.length > 0) { setWord([...word.slice(0,-1)]); - setCharBox(prevState => { + setInputBox(prevState => { const newState = [...prevState]; newState[word.length-1] = <CharBox char={""} />; return newState; @@ -49,7 +84,11 @@ const WordInput = () => { return ( <div className="word-input"> - {charBox} + {allWords.slice(0, currentTry)} + <div className="word-box input-box"> + {inputBox} + </div> + {allWords.slice(currentTry+1, NUM_TRY+1)} </div> ); } diff --git a/src/components/style/Boxes.css b/src/components/style/Boxes.css new file mode 100644 index 0000000000000000000000000000000000000000..42ab73e42561e62d4f93b7fb238843777db0e3de --- /dev/null +++ b/src/components/style/Boxes.css @@ -0,0 +1,30 @@ +.char-box { + width: 50px; + height: 50px; + display: inline-block; + text-align: center; + vertical-align: middle; + line-height: 50px; + margin: 5px; + border: 1px solid black; +} + +.correct-box { + background-color: green; + color: white; +} + +.misplaced-box { + background-color: yellow; + color: black; +} + +.incorrect-box { + background-color: red; + color: white; +} + +.default-box { + background-color: white; + color: black; +} \ No newline at end of file diff --git a/src/helpers/validateInput.js b/src/helpers/validateInput.js index 23fc32089babc3dc914f784b3585f4aa98bb8c08..d4a5a19c5243811dbef9c56a0e9c451ff00065f1 100644 --- a/src/helpers/validateInput.js +++ b/src/helpers/validateInput.js @@ -1,28 +1,57 @@ -export function validateInput(input, actual) { - let initInput = input.split(""); - let evaluateInput = actual.split(""); +import axios from 'axios'; +import { findIndex, includes, zipWith } from 'ramda'; - const mappedInput = initInput.map((e, idx) => { - if (e === actual[idx]) { - evaluateInput.splice(idx, 1); - return "Correct"; - } else { - const filteredActual = evaluateInput.map((elementActual, idxActual) => { - if (elementActual === e) { - return [elementActual, idxActual]; - } else { - return undefined; - } - }).filter(x => x); +export async function validateInput(input, actualString) { + const isValidLength = checkInputLength(input, 5); + if (!isValidLength) { + return { "result": null, "error": "Not enough characters" }; + } - if (filteredActual.length === 0) { - return "Wrong"; - } else if (e === filteredActual[0][0]) { - evaluateInput.splice(filteredActual[0][1], 1) - return "Misplaced"; - } + const isInputDefined = await checkInputDefined(input); + if (isInputDefined["res"] === false) { + return { "result": null, "error": "Word is not defined" }; + } else if (isInputDefined["error"]) { + return { "result": null, "error": "Validation error: " + isInputDefined["error"] }; + } + + let actual = actualString.split(""); + + const checkCorrect = (i, a) => { + const status = (i === a) ? "correct" : "incorrect"; + return status; + } + const firstEvaluation = zipWith(checkCorrect, input, actual); + + const lastEvaluation = firstEvaluation.map((e, idx) => { + const word = input[idx]; + if (e === "correct") { + actual[idx] = ''; } + else if (e === "incorrect" && includes(word, actual)) { + const actualIdx = findIndex((i) => word === i)(actual); + actual[actualIdx] = ''; + return "misplaced"; + } + return e; }); + + return { "result": lastEvaluation, "error": null }; +} - return mappedInput; +function checkInputLength(input, length) { + const isValid = (input.length === length) ? true : false; + return isValid; +} + +// TODO: Replace backend URL to deployed one +async function checkInputDefined(input) { + const response = await axios({ + method: 'get', + url: 'http://localhost:8080/check/' + input.join(""), + }); + if (response.statusText === "OK") { + return { "res": response.data, "error": null }; + } else { + return { "res": null, "error": response.statusText }; + } }