Friday, 7 June 2019

Web Tutorial: The Liverpool Quiz (Part 3/3)

It's time for the most visual part of this web tutorial - layout and user experience. The quiz is quite functional as it is right now, but no one will ever accuse it of being pretty and user-friendly. Let's change that.

Set the background to a deep red, and give quizContainer a width of 600 pixels, then move it center of the screen.

css\styles.css
body
{
    background-color: #660000;
}

#quizContainer
{
    width: 600px;
    margin: 50px auto 0 auto;
}

.pnlMain, .pnlQuestions, .pnlResults
{
    display: none;
}


What a dramatic difference!


Now set font sizes and colors. I'm partial to Verdana myself... and white seems good if we're going to use that deep red as a background.

css\styles.css
.pnlMain, .pnlQuestions, .pnlResults
{
    width: 100%;
    font-family: verdana;
    font-size: 14px;
    color: #FFFFFF;
    display: none;
}


Better already.


Let's give the panels round corners and a translucent white border. And a padding of, say, 1em.

css\styles.css
.pnlMain, .pnlQuestions, .pnlResults
{
    width: 100%;
    font-family: verdana;
    font-size: 14px;
    color: #FFFFFF;
    padding: 1em;
    border-radius: 10px;
    border: 1px solid rgba(255, 255, 255, 0.5);
    display: none;
}


Nice!


Now let's give the main and results panels a larger amount of padding by setting the divs within, styled using content, a sizeable top and bottom margin, and center the text.

css\styles.css
.pnlMain.main, .pnlQuestions.questions, .pnlResults.results
{
    display: block;
}

.pnlMain .content, .pnlResults .content
{
    margin-top: 20%;
    margin-bottom: 20%;
    text-align: center;
}


Looking presentable.


We'll work on the buttons next. First, set all divs using the class buttons, to a 100% width and 3em height. Then for all buttons styled using btn, float them right. I've also given them rounded corners, white text, a scarlet background, padding and margins. And used a hover psudoselector to turn the background bright red if the user mouse the cursor over the button. Feel free to impose your own style.

css\styles.css
.pnlQuestions .content.current
{
    display: block;
}

.buttons
{
    width: 100%;
    height: 3em;
}

.btn
{
    float: right;
    width: 8em;
    border-radius: 5px;
    border: 0px solid #FFFFFF;
    padding: 0.5em;
    margin-left: 0.5em;
    font-weight: bold;
    color: #FFFFFF;
    background-color: #AA0000;
    cursor: pointer;
}

.btn:hover
{
    background-color: #FF0000;
}


Getting there, folks!


Let's move on to the questions section. Now you should see that the text isn't centered because we only centered text for the main and results panels. The main problem here is with the selectors. They don't really respond when moused over or clicked on. So let's first impose a hover pseudoselector on optionSelector, turning the border a deeper grey when the cursor moves over.

css\styles.css
.optionSelector
{
    width: 0.8em;
    height: 0.8em;
    border: 1px solid #AAAAAA;
    cursor: pointer;
    border-radius: 50%;
    float: left;
    background-color: transparent;
}

.optionSelector:hover
{
    border: 1px solid #999999;
}


Next, if the div is styled using both optionSelector and selected, change background color and border.

css\styles.css
.optionSelector:hover
{
    border: 1px solid #999999;
}

.optionSelector.selected
{
    background-color: #440000;
    border: 1px solid #000000;
}


In the HTML, add this template string. It's populated using the selected property of the current option object.

index.html
                        <div class="answers">
                            <div class="option" ng-repeat="option in question.options">
                                <div class="selector">
                                    <div class="optionSelector {{option.selected}}" ng-click="selectOption($parent.$index, $index)"></div>
                                </div>

                                <div class="text">
                                    <div class="optionText">{{option.text}}</div>
                                    <div class="optionExplanation {{isAnswered($parent.$index)}} {{getIndicator($parent.$index, $index)}}">{{question.explanation}}</div>
                                </div>
                            </div>
                        </div>


Now if you click on any of the options, you can tell right off!


But this isn't good enough. What we also need is an indicator to tell us if the answer is correct or wrong. Add a div here. Style it using optionIndicator, and add a template string using the getIndicator() function. Pass in the appropriate arguments. Also add in a template string using the selected property of the current option object.

index.html
                        <div class="answers">
                            <div class="option" ng-repeat="option in question.options">
                                <div class="selector">
                                    <div class="optionIndicator {{option.selected}} {{getIndicator($parent.$index, $index)}}"></div>
                                    <div class="optionSelector {{option.selected}}" ng-click="selectOption($parent.$index, $index)"></div>
                                </div>

                                <div class="text">
                                    <div class="optionText">{{option.text}}</div>
                                    <div class="optionExplanation {{isAnswered($parent.$index)}} {{getIndicator($parent.$index, $index)}}">{{question.explanation}}</div>
                                </div>
                            </div>
                        </div>


Write the optionIndicator CSS class. Use the before psudoselector to set content to an empty string by default.
css\styles.css
.btn:hover
{
    background-color: #FF0000;
}

.optionIndicator
{
    width: 1.5em;
    height: 1.5em;
    float: left;
}

.optionIndicator:before
{
    display: block;
    content: "";
    font-size: 1.5em;
}

.optionSelector
{
    width: 0.8em;
    height: 0.8em;
    border: 1px solid #AAAAAA;
    cursor: pointer;
    border-radius: 50%;
    float: left;
    background-color: transparent;
}


If the div is styled using optionIndicator and correct, the content is a bright green tick. If not, it's a bright red cross.

css\styles.css
.optionIndicator
{
    width: 1.5em;
    height: 1.5em;
    float: left;
}

.optionIndicator:before
{
    display: block;
    content: "";
    font-size: 1.5em;
}

.optionIndicator.selected.correct:before
{
    display: block;
    content: "\2713";
    color: #00FF00;
}

.optionIndicator.selected.wrong:before
{
    display: block;
    content: "\2717";
    color: #FF0000;
}


See that? So cool.




Let's clean stuff up by styling question, option, selector and text. questionText is styled as well, to set font size and such.

css\styles.css
.question
{
    width: 100%;
    height: 6em;   
}

.questionText
{
    width: 70%;
    float: left;
    font-size: 1.5em;
}

.option
{
    width: 100%;
    height: 3em;
}

.selector
{
    width: 3em;
    float: left;
}

.text
{
    width: 90%;
    float: left;
}

.optionIndicator
{
    width: 1.5em;
    height: 1.5em;
    float: left;
}


Coming along nicely!


Styling the timer

The timer is a little bare bones now, but we can beautify it. We're going to make it a brown circle with a translucent black background. Styling the paragraph tag within timer helps adjust the counter value a bit.

css\styles.css
.btn:hover
{
    background-color: #FF0000;
}

.timer
{
    width: 2em;
    height: 2em;
    font-size: 1.5em;
    font-weight: bold;
    text-align: center;
    border-radius: 50%;
    border: 3px solid #440000;
    background-color: rgba(0, 0, 0, 0.5);
    margin-top: -1.5em;
    margin-left: -1.5em;
}

.timer p
{
    margin-top: 0.25em;
}

.question
{
    width: 100%;
    height: 6em;   
}


Great!


Adding a progress indicator

It's better to do a quiz when you have an idea just how far you are along. Add template strings. currentQuestion needs to have 1 added because the count starts from 0.

index.html
                        <div class="question">
                            <div class="questionText">
                                {{question.text}}
                            </div>

                            <div class="questionProgress">
                                {{currentQuestion + 1}} of {{possible}}
                            </div>   
                        </div>


Some styling.

css\styles.css
.questionText
{
    width: 70%;
    float: left;
    font-size: 1.5em;
}

.questionProgress
{
    width: 30%;
    float: right;
    text-align: right;
}

.option
{
    width: 100%;
    height: 3em;
}


Yep, that's just what we're going for.


At this point, you may have noticed that the text needs more contrast. My bad. Style optionText and expand on the styling for optionExplanation. We'll make the text smaller and change the color to orange.

css\styles.css
.optionSelector.selected
{
    background-color: #440000;
    border: 1px solid #000000;
}

.optionText
{
    width: 100%;
    height: 1.5em;
    font-size: 0.9em;
}

.optionExplanation
{
    width: 80%;
    height: 2.5em;
    visibility: hidden;
    font-size: 0.7em;
    color: #FF4400;
}

Got that?


Back to the buttons...

Having each button say "Next" is well and good, but it would be better if the button says "Results" if it's the final question. To do that, modify the selectOption() function. Check if currentQuestion is less than the length of the questions array, minus 1. Essentially, that it's not the final question. If it's the final question, change proceed to "Results".

js\main.js
    $scope.selectOption =
    function (question, option)
    {
        if ($scope.isAnswered(question) != "answered")   
        {
            $scope.questions[question].options[option].selected = "selected";

            if (option == $scope.questions[question].answer)
            {
                $scope.result++;
            }

            if ($scope.currentQuestion < $scope.questions.length - 1)
            {
                $scope.proceed = "Next";
            }
            else
            {
                $scope.proceed = "Results";
            }
        }   
    };


Ah, it's the little things, amirite?


Also, since we've coded it in such a way that you can only go on to the next question after answering the current question, let's make it even more obvious via styling. Render btnNext in a darker color by default. Only use the standard colors if btnNext is also styled using answered.

css\styles.css
.btn:hover
{
    background-color: #FF0000;
}

#btnNext.btn
{
    color: #000000;
    background-color: #440000;
}

#btnNext.btn.answered
{
    color: #FFFFFF;
    background-color: #AA0000;
}

#btnNext.btn:hover
{
    background-color: #440000;
}

#btnNext.btn.answered:hover
{
    background-color: #FF0000;
}


Then add this template string to the btnNext button's class attribute, using the isAnswered() function with currentQuestion used as an argument.

index.html
                <div class="buttons">
                    <input type="button" id="btnNext" class="btn {{isAnswered(currentQuestion)}}" value="{{proceed}}" ng-click="nextQuestion()">
                    <input type="button" id="btnQuit" class="btn" value="Quit" ng-click="initQuiz()">
                </div>


There. Now it's more obvious that "Next" won't respond to clicks!


At the end

The message is a little sketchy on detail. We'll fix that. In the case of getting to the results panel, there are two ways to get there - either the timer runs out, of you complete the quiz. So we check if secodnsRemaining is greater than 0. In either case, change secondsRemainingMessage.

js\main.js
    $scope.getStage =
    function ()
    {
        if ($scope.currentQuestion == -1) return "main";

        if ($scope.currentQuestion == $scope.questions.length)
        {
            if ($scope.secondsRemaining > 0)
            {
                $scope.secondsRemainingMessage = "That was fast!";
            }
            else
            {
                $scope.secondsRemainingMessage = "Time's up!";
            }

            return "results";
        }

        return "questions";
    };


Then check the score. Wouldn't it be nice to have a message based on the score? So let's check result and see if the user scored more than 50%. Change resultsMessage based on that. Also change resultsGrade because we'll be using that soon.

js\main.js
    $scope.getStage =
    function ()
    {
        if ($scope.currentQuestion == -1) return "main";

        if ($scope.currentQuestion == $scope.questions.length)
        {
            if ($scope.secondsRemaining > 0)
            {
                $scope.secondsRemainingMessage = "That was fast!";
            }
            else
            {
                $scope.secondsRemainingMessage = "Time's up!";
            }

            if ($scope.result >= ($scope.possible / 2))
            {
                $scope.resultsMessage = "Well done!";
                $scope.resultsGrade = "good";
            }
            else
            {
                $scope.resultsMessage = "Do better next time!";
                $scope.resultsGrade = "poor";
            }

            return "results";
        }

        return "questions";
    };

Getting there!


Now for some pictures!

The layout is boring without them. I mean, this is a Liverpool FC Quiz, right? Add this to the styling for the panels. This basically fixes background images.

css\styles.css
.pnlMain, .pnlQuestions, .pnlResults
{
    width: 100%;
    font-family: verdana;
    font-size: 14px;
    color: #FFFFFF;
    padding: 1em;
    border-radius: 10px;
    border: 1px solid rgba(255, 255, 255, 0.5);
    background-size: cover;
    background-position: center center;
    background-repeat: no-repeat;
    display: none;
}


I'm gonna use this pic that I downloaded and doctored, and save it in the img folder. In it, it looks like Luis Garcia scored a goal or something.

main.jpg


And then let's embed the background in the HTML.

index.html
<div class="pnlMain {{getStage()}}" style="background-image: url(img/main.jpg)">

There it is...


There are a lot of pics I'm using for questions. I won't bother uploading them here. You can find them in the repository.

We do the same for the questions panel, only we embed a template string in the background image file name.

index.html
<div class="pnlQuestions {{getStage()}}" style="background-image: url(img/q{{currentQuestion}}.jpg)">


See what we did there? A question about Fernando Torres has a background picture of... Fernando Torres!


Now for the results panel. This one is slightly more interesting. I have two pics...

results_good.jpg

results_poor.jpg


And here I'll use resultsGrade in a template string for the embdedded background image.

index.html
<div class="pnlResults {{getStage()}}" style="background-image: url(img/results_{{resultsGrade}}.jpg)">


And here we see the results! In the "poor" result, we get a picture of Sadio Mane looking really dejected. In the "good" result, Virgil Van Djik is being mobbed by the team.



Final Football Note

I finished writing this web tutorial just days after Liverpool won their 6th UEFA Champions League trophy. Still positively buzzing, so forgive me if the material comes up short.

You'll Never Code Alone,
T___T

No comments:

Post a Comment