Monday 20 June 2022

Web Tutorial: The Japanese Language Trainer App (Part 2/4)

Now that we have a default view for the Display component, let's add alternative ones. We begin by adding a case for if the exercise has started, there are remaining questions and a question has been defined.

src/components/Display/Display.js
function Display(props) {
    let charset = props.charset;
    let started = props.started;
    let remaining = props.remaining;
    let maxRemaining = props.maxRemaining;
    let answer = props.answer;
    let question = props.question;
    let result = props.result;
    let usedQuestions = props.usedQuestions;
    let setRemaining = props.setRemaining;
    let setAnswer = props.setAnswer;
    let setQuestion = props.setQuestion;
    let setUsedQuestions = props.setUsedQuestions;

    if (started && remaining > 0 && charset[question] !== undefined) {
        return (
           
        );
    }

    
    return (
        <>
            <br />
            <h3>Welcome to J-Trainer!</h3>
            <ol>
                <li>Select your character set.</li>
                <li>Click <button className="btnSmall">BEGIN ➤ </button> to start.</li>
                <li>For every character that is displayed, select the correct pronounciation. Note that some characters may have the same pronounciation as others.</li>
                <li>Click the <button className="btnSmall">REMAINING ➤ </button> button to continue after the results are displayed.</li>
            </ol>
        </>            
    );
}


In that case, we have three divs, styled using CSS classes Character, Result (and Hide depending on whether answer is an empty string) and Remaining. The last one will have a button which will show how many questions are remaining, less the one question that is on display.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">

            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>

            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>  
        
    );
}


In the first div, we will display the char property of the element of charset pointed to by question. In the second div, we display the answer.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


We will also add a span tag, and the CSS class is Correct or Wrong based on the comparison of answer with the romaji property of the element of charset pointed to by question.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>

                </span>

                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


A tick or cross is also displayed based on the same criteria.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


Now, you won't see anything new yet. That's because question is null at this point, thus the element of charset pointed to by question is going to be undefined. We need to set a question, and will do so by adding this If block to check if question is null and charset is not. If so, we run ManageQuestionList().

src/components/Display/Display.js
let setQuestion = props.setQuestion;
let setUsedQuestions = props.setUsedQuestions;

if (question === null && charset !== null) {
    ManageQuestionList();
}

    
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


And here we define the function.

src/components/Display/Display.js
let setQuestion = props.setQuestion;
let setUsedQuestions = props.setUsedQuestions;

const ManageQuestionList = ()=> {

}

    
if (question === null && charset !== null) {
    ManageQuestionList();
}
    
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


In here, we define index by getting one random number from 0 to the size of charset.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);
}


And then we check if index is in the array usedQuestions. We keep getting that random number as long as it already exists in usedQuestions. This is so that we don't get repeats in the same exercise.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);

    while (usedQuestions.indexOf(index) !== -1) {
        index = Math.floor(Math.random() * charset.length);
    }

}


Now we use the setter setQuestion() to set question to the value of index.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);

    while (usedQuestions.indexOf(index) !== -1) {
        index = Math.floor(Math.random() * charset.length);
    }

    setQuestion(index);
}


And we make sure usedQuestions has index in it.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);

    while (usedQuestions.indexOf(index) !== -1) {
        index = Math.floor(Math.random() * charset.length);
    }

    setQuestion(index);
    let questionList = usedQuestions;
    questionList.push(index);
    setUsedQuestions(questionList);

}


So yeah! Let's try this again. You should now see that tiny character, randomly picked from charset. This will also depend on whether you chose "Hiragana" or "Katakana".




This obviously needs styling! We go to Display.css next. For the Character CSS class, what I have done is enlarge the text, frame it in a light grey circular border, and adjust the layout.

src/components/Display/Display.css
.Character{
    font-size: 10em;
    text-align: center;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background-color: #AAAAAA;
    color: #FFFFFF;
    margin: 0 auto 0 auto;
    padding: 0px;
}


For Result, it's pretty much just for appearance as well.

src/components/Display/Display.css

.Character{
    font-size: 10em;
    text-align: center;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background-color: #AAAAAA;
    color: #FFFFFF;
    margin: 0 auto 0 auto;
    padding: 0px;
}

.Result{
    font-size: 2em;
    font-weight: bold;
    text-align: center;
}


I have set Remaining to center text. You should see the button centered just so. The CSS classes Correct and Wrong have been assigned colors. You won't see the results just yet though.

src/components/Display/Display.css
.Character{
    font-size: 10em;
    text-align: center;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background-color: #AAAAAA;
    color: #FFFFFF;
    margin: 0 auto 0 auto;
    padding: 0px;
}

.Result{
    font-size: 2em;
    font-weight: bold;
    text-align: center;
}

.Remaining{
    text-align: center;
}

.Correct{
    color: #009900;
}

.Wrong{
    color: #990000;
}


And now you see this!




The Keyboard component

What we need next is a way for the user to input the correct answer. For that, we will have a keyboard with buttons, all labelled with the eighty-six different possible pronounciations in both Hiragana and Katakana.

So create the Keyboard directory inside the components directory, and create Keyboard.js, Keyboard.css and index.js. For index.js, we are doing just what we did for the other index files.

src/components/Keyboard/index.js
export { default } from './Keyboard';


In Keyboard.js, we import the CSS file, define Keyboard as a function, and export Keyboard as a component.

src/components/Keyboard/Keyboard.js
import './Keyboard.css';

function Keyboard(props) {

}

export default Keyboard;


And here, we define variables based on the various values passed from props.

src/components/Keyboard/Keyboard.js
import './Keyboard.css';

function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;

}

export default Keyboard;


We define keyItems by calling GetKeys().

src/components/Keyboard/Keyboard.js
import './Keyboard.css';

function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;
    
    const keyItems = GetKeys();
}

export default Keyboard;


We are going to import GetKeys from a utility we are going to write next.


src/components/Keyboard/Keyboard.js
import './Keyboard.css';

import GetKeys from '../../utils/GetKeys';

function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;
    
    const keyItems = GetKeys();
}

export default Keyboard;


Create this file in the utils directory. We need to define GetKeys() and export it. Then within the function, define keys as an array and return it.

src/utils/GetKeys.js
const GetKeys = () => {
    let keys = [];

    return keys;
}

export default GetKeys;    


This is the layout that we are going to use. There are 15 columns and 6 rows. This is just a big-ass one-dimensional array of (15 x 6 = 90) elements, but I have visually arranged the code so we can see what the final layout will look like. Note that there are several empty strings among the array elements. These represent the gaps.

src/utils/GetKeys.js
const GetKeys = () => {
    let keys = [];

    keys = [
        "pa", "ba", "da", "za", "ga", "wa", "ra", "ya", "ma", "ha", "na", "ta", "sa", "ka", "a",
        "pi", "bi", "", "ji", "gi", "", "ri", "", "mi", "hi", "ni", "chi", "shi", "ki", "i",
        "pu", "bu", "", "zu", "gu", "", "ru", "yu", "mu", "fu", "nu", "tsu", "su", "ku", "u",
        "pe", "be", "de", "ze", "ge", "", "re", "", "me", "he", "ne", "te", "se", "ke", "e",
        "po", "bo", "do", "zo", "go", "wo", "ro", "yo", "mo", "ho", "no", "to", "so", "ko", "o",
        "", "", "", "", "", "n", "", "", "", "", "", "", "", "", ""
    ];


    return keys;
}

export default GetKeys;    


Now back to the Keyboard component. Now we are going to define answers as an entire HTML block that we will be generating from keyItems, using the map() method. (Here's some information on this method.)

src/components/Keyboard/Keyboard.js
function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;
    
    const keyItems = GetKeys();
    
    const answers = keyItems.map((item, index) => (

    );

}


We want a div for every element in keyItems, which, as we've defined, is an array. Putting in item within the div will give us divs with the strings from keyItems.

src/components/Keyboard/Keyboard.js
const answers = keyItems.map((item, index) => (
    <div>
        {item}                    
    </div>

    )
);


We will add a key for each element just to avoid the warnings ReactJS will inevitably give us otherwise. The CSS class will be Answer and Hide if item is an empty string (remember the empty strings we put in the GetKeys() function?) and if not, we do a further check if answer is an empty string. If the question has not been answered, the CSS class is Answer, but if it has, we grey it out using Greyed as well.

src/components/Keyboard/Keyboard.js
const answers = keyItems.map((item, index) => (
    <div
        key={'romaji_' + index}
        className={item === '' ? 'Answer Hide' : ( answer === '' ? 'Answer' : 'Answer Greyed')}

    >
        {item}                    
    </div>
    )
);


We next check if started is true and there are questions remaining. If so, we display answers. If not, we don't.

src/components/Keyboard/Keyboard.js
    const answers = keyItems.map((item, index) => (
        <div
            key={'romaji_' + index}
            className={item === '' ? 'Answer Hide' : ( answer === '' ? 'Answer' : 'Answer Greyed')}
        >
            {item}                    
        </div>
        )
    );

    if (started && remaining > 0) {
        return (
            <>
                { answers }
            </>            
        );
    } else {
        return (
            <>
            </>            
        );
    }

}

export default Keyboard;


In App.js, add an import statement.

src/App.js
import React, { useState } from 'react';
import './App.css';
import Header from './components/Header';
import Display from './components/Display';
import Keyboard from './components/Keyboard';

function App() {


Inside the div styled using Bottom, include the Keyboard component and pass in the properties.

src/App.js
<div className="Bottom">
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />

</div>


Obviously not ideal. Time for some CSS!




Each divs should be 35 pixels across. This is important because the width of the div is 600 pixels, and you want the row to only have 15 buttons. With top and right margins set to 3 pixels, this should be adequate. float property is set to left, and cursor is pointer to indicate that this div is clickable. The rest is aesthetics - round borders, grey borders and text, and a white background.

src/components/Keyboard/Keyboard.css
.Answer {
    width: 35px;
    height: 30px;
    float: left;
    cursor: pointer;
    border: 1px solid #444444;
    border-radius: 5px;
    color: #444444;
    background-color: #FFFFFF;
    text-align: center;
    font-weight: bold;
    margin-top: 3px;
    margin-right: 3px;
}


We have a hover pseudoselector for the button to change color upon a mouseover. The Greyed class has cursor set to default and the background changed, to show that the button is no longer clickable.

src/components/Keyboard/Keyboard.css
.Answer {
    width: 35px;
    height: 30px;
    float: left;
    cursor: pointer;
    border: 1px solid #444444;
    border-radius: 5px;
    color: #444444;
    background-color: #FFFFFF;
    text-align: center;
    font-weight: bold;
    margin-top: 3px;
    margin-right: 3px;
}

.Answer:hover {
    color: #FFFFFF;    
    background-color: #444444;
}

.Greyed, .Greyed:hover {
    border: 1px solid #AAAAAA;
    color: #AAAAAA;
    background-color: #FFFFFF;
    cursor: default;
}


And here's your keyboard! I have moused over "ya" to show you the effect.




Next

Allowing users to input answers, and showing the results.

No comments:

Post a Comment