Hello, my name is: Amy

The Quiz App

Mostly for the benefit of my future-self, I'm going to step through my first project at All Things Media, the conversion of a flash quiz to HTML5. This project certainly wasn't easy for me, but was unbelievably fun as I began to see my self stepping one by one through each problem I ran into. You might want to get the popcorn ready, this post is going to be a long one.

The Quiz

The Flash Original

This was a simple quiz with a little razzle dazzle in the animation/sounds department. I was just given the flash links to peruse, but none of the original assets, logic, or copy. There were currently four quizzes available, and we were to replicate those as well as to provide a CMS where the client could create and manage the quizzes. Our quizzes also needed to be all-device friendly and have randomized questions each time a quiz was taken. I had recently been studying up on using AngularJS so I decided to make use of that knowledge on this project. Again, probably not a decision that was necessary.

Below is a video walking through a very basic quiz from the final html quiz. These are made up quizzes just meant to illustrate a point.

Look and Feel

I started off by recreating the visual look of the question/answers. It was something I was comfortable with doing, so this was pretty easy to get the general setup of as it was a fairly clean design. It consisted of just a few layouts:

  1. List of All Quizes
  2. Introduction Quiz Page
  3. Quiz Questions/Answers
  4. Calculating
  5. Results Page

I built all the layouts separately with a css setup that could get awards for being extraordinarily complicated. Note for next time, and probably the overarching theme of this project:

KISS. Keep it simple, superhero.

What had happened was I had just read this article about great ways to organize SASS partials, so I thought: YES, this needs to be used. Which led me down a path that would leave me with (no lie) twelve _partial.scss files for this simple little site and ended up being way more confusing than it was beneficial.

I've since heard some people say that perhaps you should start with using .css only, until the need for a pre-processor arises. Then you should make the switch. I'm not sold on that approach yet but I definitely agree that it's smart to start with one well organized .scss file and then split from there as needed.

Start with ONE .css/.scss file, then split up as it becomes unmanageable.

So, with the base, static layout in tact I was free to move on to some of the more delicate intricacies, and what I thought was going to be the most challenging were the animations.

Reverse Engineering The Quiz

This is where my experience with Seventeen magazine quizzes (Are you a ...?) all those years ago really paid off. After going through the provided quizzes a ton of times, I was able to determine that for 3 of the 4 quizzes that each of the answers had a (pretty obvious) link to one of the quiz results and that there were the same number of quiz results as there were answers per question. (One quiz was far to obscure for me to successfully trace back.) From there, it seemed I'd just need to tally the answers and then the result with the highest tally wins.

Three of the quizzes I was provided with had four answers per question, while one had only three. Therefore I needed to make the system flexible to allow the quiz administrator to choose the number of quiz results first and have a corresponding number of question selections available which could allow for quizzes with 2, 3, or 4 quiz results.

Data Structure

My goal next was to set to work on planning where all this data was going to live. My experience with databases to date had really only been with one or two tables with no relation to each other. Because the results needed to somehow be linked to the questions and the questions needed to be tied to each quiz I knew multiple relational tables would be needed. So I figured that meant a few different tables:

  1. Users (for CMS)
  2. Quizzes - list of quizzes available (title, level, description, etc)
  3. Questions/Answers - Questions and each of up to four of the answers with appropriate keys.
  4. Results
  5. Levels

In retrospect after creating this monster, it might have been smarter to separate the questions and answers table I ended up running into a few fights with getting the answers to act like I needed because I was forced into displaying 'answer1' and 'answer2' etc, rather than looping through the available answers per quiz.

I ended up using MySQL Workbench for the first time to set up a visual Database Design. This helped me me create the foreign keys that linked these tables together. I'm sure there could have been a lot more to do with MySQL Workbench, but this was really all the use I garnered.

Turns out, this visual Database Design has no tie in if you update your actual database as I sent this file to our team building the CMS and we ended up having discrepancies in our databases. (oops.)

If I were to do this project again, I might actually just create the .json output that I needed first, without really worrying so much about the database structure. That way I can make sure that what the application is running on is slim and as efficient as possible.

Calculating the Results

This is where I (first) repeatedly bumped my head against the wall. It certainly didn't help that I struggled with basic concepts like knowing how to store and retrieve data from objects and arrays. I'd ask my manager, Mike, how he'd approach something, and then I'd have to think for a solid night about what he said and translate it into an approach I somewhat could wrap my head around.

I had an answer key value to all the answers so I needed to log that value in order to start the tally. What I ended up with was a global variable called results. Since I had set up the database with a known four answers, I used an array to store these results by pushing the value in.

It took me more than a few thoughts to realize that I global variables involved so things aren't continuously overwritten. What I didn't realize though was that I probably could have made use of more than the one. What's happening here is I'm storing all the results into the global variable array allResults. So, for a ten question quiz I'd have allResults = [0,1,1,3,0,3,1,1,2,0].

//Global var to store results
var allResults = [];

//Reset results if someone restarts quiz
var resetResults = function () {
  allResults = [];
};

Inside my calcNext() function that was executed each time the user selected an answer, I passed in the result key (result) and the question index:

this.calcNext = function (result,index) {
  //Results calculation
  $scope.myResults = allResults;
  var resultsTally = [0,0,0,0]; //initializes results tally array for 4 values

  var numResult = Number(result); //make result a number
  allResults.push(numResult); //push to global array

  var allResultsSize = $scope.myResults.length; //determines size of allResults

  //enter in results for quiz into 'resultsTally'
  for (var x = 0; x < allResultsSize; x++) {
    if ($scope.myResults[x] == 0) {
        resultsTally[0]++;
    } else if ($scope.myResults[x] == 1) {
      resultsTally[1]++;
    } else if ($scope.myResults[x] == 2) {
      resultsTally[2]++;
    } else if ($scope.myResults[x] == 3) {
      resultsTally[3]++;
    } else {
      alert("Oops! Somethings not right.");
      $state.go('quizzes'); //Stop quiz as results not logging correctly
    }
  }
}

As you may have noticed, each time someone chooses an answer, the tally of those results, resultsTally, is recalculated. It certainly seems like I could have also stored the resultsTally as a global variable. Then, instead of looping through the whole array every time and recalculating the tally, I could have just bumped up the value of the tally for the result that was logged. Next time, next time.

After the last question was submitted and the final result was to be calculated, I used Math.max() to determine which one was the highest, and if there was a tie it would take the left most.

var highestResponse = getMaxOfArray(resultsTally);

//value for response
$scope.responseIndex = resultsTally.indexOf(highestResponse);

I'm using this responseIndex in order to filter the ng-repeat of the response page to only show that particular result.

ng-repeat="data in quizc.results| filter : {quiz_slug: quizc.quiz_slug, result_key: responseIndex}"

Routing

I used UI-Router to handle the routing of this quiz. My thought was to be able to natively use the routing to pull the quiz id out of the URL but perhaps there are better ways to do this. I ended up fighting with it a bit to have the page flow like I needed but I suspect this was more due to my lack of understanding.

//UI-Router Info
app.config(function($stateProvider, $urlRouterProvider) {

  // For any unmatched url, redirect to:
  $urlRouterProvider.otherwise("/quizzes");

  // Setting up the states
  $stateProvider
    .state('quizzes', {
      url: "/quizzes",
      templateUrl: "partials/quizzes.html"
    })
    .state('home', {
      url: "/{quiz_slug}",
      templateUrl: "partials/home.html"
    })
    .state('questions', {
      url: "/{quiz_slug}/q",
      templateUrl: "partials/questions.html"
    })
    ;
});

Audio

I pulled the sounds off the flash app by recording using Audacity while taking the quiz. I need to find out if there is a global Audio object. That's what it seems like but I'm not sure where it's coming from. Also I need to figure out what's happening with checkAudioType

//Audio
var intro_sound;

//determines if browser can play mp3 format
var checkAudioType = function () {
  var a = document.createElement('audio');
  return !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
}

if (checkAudioType()) {
  intro_sound = new Audio('assets/audio/intro.mp3');
} else {
  intro_sound = new Audio('assets/audio/intro.ogg');
}
$scope.play_intro = function () {
  intro_sound.play();
}  

//Audio
var intro_sound;
var button_click;
var answer_click;
var hover_sound;
var calc_sound;

//determines if browser can play mp3 format
var checkAudioType = function () {
  var a = document.createElement('audio');
  return !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
}

if (checkAudioType()) {
  button_click = new Audio('assets/audio/button_click.mp3');
  answer_click = new Audio('assets/audio/select_answer.mp3');
  hover_sound = new Audio('assets/audio/hover.mp3');
  calc_sound = new Audio('assets/audio/result_display.mp3');
  intro_sound = new Audio('assets/audio/intro.mp3');
} else {
  button_click = new Audio('assets/audio/button_click.ogg');
  answer_click = new Audio('assets/audio/select_answer.ogg');
  hover_sound = new Audio('assets/audio/hover.ogg');
  calc_sound = new Audio('assets/audio/result_display.ogg');
  intro_sound = new Audio('assets/audio/intro.ogg');

}
$scope.play_button = function () {
  button_click.play();
}
$scope.hover = function () {
  hover_sound.play();
}
$scope.play_intro = function () {
  intro_sound.play();
}

Adding Flair with Animations

I had to manage state to determine the active question to know how things should animate.

//Get Quiz Data
indivquizdata = quiz.items;
quizLength = indivquizdata.length; //length of all quizzes
$scope.count = 0; //determines total # of questions per quiz

//loops through all quizzes records and finds count of those = quizzes_id
for (var x = 0; x < quizLength; x++) {
  if (quiz.items[x].quiz_slug == this.quiz_slug)
    $scope.count++;
}

if(this.currentQuestion < ($scope.count - 1)) {
  this.currentQuestion += 1;
} else {
  $scope.quizOver = true;
  $timeout(startCalculation, 2400); //waits til anim. complete
}

The answer the user selects has a different animation than the others, which I had tremendous amounts of problems with. I think it was mostly because I wasn't using a ng-repeat on the answers, so I couldn't control the function with a normal $index to selectively apply a class if certain criteria was met.

I ended up solving it by applying this class below. Here, the $index is the question number. So, I'm checking to see if the value in myReults for the current question is equal to that answers key. If so, it gets the class clicked and can animate accordingly.

{{myResults[$index] == data.a1_key ? 'clicked' : ''}}

After applying the class successfully, I then struggled with fighting with the css animation for how to handle this clicked business. After many thoughts about the choice of my future as a web developer, specificity to the rescue:

//animates exit of questions
.anim-answ {
  @include animation(rev-slideover .6s ease-in-out forwards); 

  &.a1 {
    @include animation-delay(1.3s);
  }
  &.a2 {
    @include animation-delay(1.2s);
  }

  &.a3 {
    @include animation-delay(1.1s);
  }

  &.a4 {
    @include animation-delay(1s);
  }
}

.answer.clicked.anim-answ {
  @include animation(shrinkdown .3s ease 0s 1 normal forwards);
}

The circles of the quiz had three states: unanswered, current, answered. To manage these states I used css classes with an icon font and passed the $index of the question into the function to determine the appropriate return.

To display the correct number of circles I used a ng-repeat to loop through the questions and insert the list item for each one. Here I'm filtering that repeat for the quiz slug to return just the data for the quiz. As you'll see later, this was a direct output of my learning curve and totally unnecessary.

For the longest time I could not get the very last question to appear as checked. It was a classic example of a problem that seemed too big for me, then I was in a spot where it had to be done, so I sat down and thought about it and it ended up taking less that 15 minutes to sort out.

//navigation classes
this.calcNavbarClass = function (index) {
  if ($scope.quizOver == true) {
    return 'fa-check-circle'; //answered
  } else if (Number(this.currentQuestion) === index) { 
    return 'fa-circle-thin'; //current
  } else if (Number(this.currentQuestion) > index) { 
    return 'fa-check-circle'; //answered
  } else {
    return 'fa-circle'; //future
  }
};
<div class="nav">
<ul class="">
  <li ng-repeat="data in quizc.items | filter : {quiz_slug: quizc.quiz_slug}"><i class="fa  {{quizc.calcNavbarClass($index);}} " ></i></li>
</ul>
</div>

The Answers

This was a doozie. And definitely confusing to read back though. This is the mess that was the code for each text answer. Such craziness going on.

<div class="text-answers {{answered}}">
<div class="tilt-r">
    <div ng-show="data.a1_text" ng-click="isClickEnabled ? quizc.calcNext(data.a1_key,$index) : null" ng-mouseover="isClickEnabled ? hover() : null" class="ieanimate {{myResults[$index] == data.a1_key ? 'clicked' : ''}} {{ ($index < quizc.currentQuestion) || quizOver ? 'anim-answ' : 'anim-current'}} answer a1 {{c1($index)}}">
      <div class="a-text">
        <p>{{data.a1_text}}</p>
      </div>
    </div>
  </div>
  <div> ...answer 2</div>
  <div> ...answer 3</div>
  <div> ...answer 4</div>
</div>

ng-click="isClickEnabled ? quizc.calcNext(data.a1_key,$index)

This bit was implemented because the user could click through the answers to the quiz before the animations had fully happened. So when the calcNext() function runs there is a timeout in place that restricts that function from running by setting isClickEnabled to false for a bit.

//allow animation to finish before re-enabling click
$scope.isClickEnabled = false;
$timeout(enableClick, 3500); 

//Click Control - Ensures that Quiz can't be rapidly clicked through.
var enableClick = function () {
  $scope.isClickEnabled = true;
};

ng-show="data.a1_text"

This would show/hide based on the type of questions being text or image.

class="ieanimate {{myResults[$index] == data.a1_key ? 'clicked' : ''}} {{ ($index < quizc.currentQuestion) || quizOver ? 'anim-answ' : 'anim-current'}} answer a1 {{c1($index)}}"

Shaking up the Answer Colors

Each question needed to have the answer colors in a different order. To do this I assigned an angular class to each of the answers that passed through the parameter of the index of the question. Then I set the value of that class with javaScript by looking up a 'random' value from a massive array, colorArray, of predetermined randomness.

class="{{c1($index)}}"

//Random colors initial values
$scope.c1 = 'c1';
$scope.c2 = 'c2';
$scope.c3 = 'c3';
$scope.c4 = 'c4';

//Set color values for next question
var colorArray = ["c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1", "c4", "c3", "c2", "c1"]; //stores colors

colorArray does have a upper limit of ~50 questions or so. But at that level of questions we might have bigger fish to fry anyhow.


$scope.c1 = function (index) {
  return colorArray[index];
}

//Now I have to make sure that the color for c2,c3,c4 aren't the same.
$scope.c2 = function (index) {
  if (colorArray[index] === 'c1') {
    return 'c2';
  } else if  (colorArray[index] === 'c2') {
    return 'c3';
  } else if  (colorArray[index] === 'c3') {
    return 'c4';
  } else {
    return 'c1';
  }
}

$scope.c3 = function (index) {
  if (colorArray[index] === 'c1') {
    return 'c3';
  } else if (colorArray[index] === 'c2') {
    return 'c4';
  } else if (colorArray[index] === 'c3') {
    return 'c1';
  } else {
    return 'c2';
  }
}

$scope.c4 = function (index) {
  if (colorArray[index] === 'c1') {
    return 'c4';
  } else if (colorArray[index] === 'c2') {
    return 'c1';
  } else if (colorArray[index] === 'c3') {
    return 'c2';
  } else {
    return 'c3';
  }
}

When I started this I had a function that would calculate a random number, then pull from the array of just the four colors. I forget why it didn't work, perhaps because I had trouble controlling that it shouldn't be the same as the previous value which I had stored in another variable.

This is certain not the most elegant solution, but, as my brother often told me, make it functional first. Then improve it. I never quite got to the improvement part, but damn if it isn't functional.

Cross Browser Testing

I used a few new css approaches (flexbox is all that is standing out now) that weren't comfortably compatible with older browsers that I needed to support. (cough, cough, IE9). I checked caniuse.com, but it turns out you also need to read the fine print.

Rotating text is hard! The original flash answers were tilted slightly in various directions. I implemented this in the beginning, but then found that my solution could be blurry in chrome, blinky in Safari, and it just ended up that the slight rotation wasn't worth the extra drama.

Mobile Friendly

Since I built this layout using percentages it was a very quick transition to making this mobile friendly. One tricky issue though was that the height of the question container is fixed so that the the progress bar circles could be positioned in the same place. On mobile this meant that if there was a short question you could potentially have a large white space gap before you got to the questions. So, for mobile I needed a solution where the height of the question would be dependent on the content.

$scope.calcQuHeight = function ( index ) {
  if (document.getElementById('qu' + (index+1) ) !== null) {
    var quBox = document.getElementById('question');
    var cuQuestion = document.getElementById('qu' + (index+1));
    var height = cuQuestion.offsetHeight;
    quBox.setAttribute("style","height:" + height + "px");
  }
};

//scroll to top (specif. for mobile)
$scope.goToTop = function() {
  // set the location.hash to the id of the element you wish to scroll to.
  $location.hash('top');
  $anchorScroll();
}

//Sets height of question box the first question for widths less than 600px
if (window.innerWidth < 600) {
  var firstCalcQuHeight = $timeout(function() { $scope.calcQuHeight(-1);},2500);
  //timeout so happens after animation concludes
}

this.calcNext = function (result,index) {
  //If mobile, re-calculate the height of the question text div
  if (window.innerWidth < 600){
    $timeout(function(){$scope.calcQuHeight(index)},1200);
  }

  //Scroll to top on next page (for mobile)
  $timeout($scope.goToTop, 2500);
}

I needed to scroll to the top so that a mobile user would start by reading each question first, rather than seeing the answers.

The CMS

Wireframing

I had a blast setting up the wireframe for how the CMS should be used. I used iPlotz and tried to think of the best workflow possible. We had another team building the CMS, so I needed to be fairly specific about conveying what needed to happen and I think that was the best way.

Testing the CMS

When implementing on a live server I had some problems with uploading Pictures to the CMS due to permissions of folders on the FTP. It was also a little tough to figure out how to allow the user to remove the picture and then add another one.

Web Services

The definitive takeaway from this program was that it is much better to do filtering with the web services than inside the application itself as you are pushing around less data. Starting out I was using my quiz-app with a large JSON file of all the quiz data that I exported using PHP. I was then filtering that in my Angular App using a filter on the ng-repeat.

I also had a .json file for quizzes, one for questions/answers, and one for results. I definitely should have structured this differently and set it up to where the script only provided one data file for just the data I needed for the given quiz.

The final implementation has a web service that should have returned one beautiful .json file, but instead I make three separate requests. (sigh.)

//sets paramater passed in through State
this.quiz_slug = $stateParams.quiz_slug; // get the slug
$scope.slug = $stateParams.quiz_slug;

//Get Data from .json files
var quiz = this; //since you can't use 'this' inside $http
quiz.details = []; //initialize to empty arrays
quiz.items = [];
quiz.results = [];

var levelSearchString = window.location.search;

$http.get('../cms/webservice/webservicequiz/?slug=' + $scope.slug).success(function (data){
    quiz.details = data;
});

$http.get('../cms/webservice/webservicequizqa/?slug=' + $scope.slug).success(function (data) {
    quiz.items = data;
}); 

$http.get('../cms/webservice/webservicequizresults/?slug=' + $scope.slug).success(function (data) {
    quiz.results = data;
});

When asked to create a web service I mentally froze up. It was hard for me to visualize what that meant exactly, and it sounded like it was a bit outside of my skill set. Our CMS team built the web service and then after seeing how they built it, it finally clicked as to what the web service was actually doing.

I wasn't so sure how the export of the .json file would look and act. Turns out it was just as simple as having a php function that queried your database that was accessible from the URL string. So, as you see above in my $http.get request, what that meant was for the main quiz data: cms/webservice/webservicequiz/?slug=' + $scope.slug. Our smarty php CMS is set up so that it finds the controller called webservice based on the URL string and within that controller finds the function webservicequiz.

function webservicequiz() {
  $slugName = $this->UrlArray['slug'];
  // Check query string parameters
  if($slugName == "")
  {
    $slugName = $this->UrlArray[2];
  }
  $slugName = urldecode($slugName);
  if(empty($slugName)){
    echo json_encode(false);
    exit;
  }
  $result = $this->model->webServiceSlug($slugName);
  echo json_encode($result);
  exit;
}

The SQL query form the Model:

function  webServiceSlug($slugName){
  $query    = "SELECT * FROM ".TABLE_QUIZ. " WHERE quiz_slug = '" . $slugName . "'";
  $this->db->Query($query);
  return $this->db->FetchAllarray();
}

The Conclusion

All in all I am incredibly proud of my first project. Of course it can be improved. But I am very happy with how it turned out. All in all it probably took me about a month to build

Comments