In this tutorial, I'll show you how to use Adventr's Realtime Player API to let viewers see how their choices compare to others.


Adventr provides a powerful and easy-to-use JavaScript API that lets you do whatever you can imagine based on viewers' engagement with your Adventr. 


In this tutorial, I'll show you how to use the Adventr Realtime Player API to show real-time quiz results as a viewer engages with an Adventr-powered quiz. You can see the demo in action here:



Here's how we built this:

We are going to listen for viewer choices and then save them to an external data source, and then display the aggregate results back to the viewer.


It might help to download the source of the demo page to follow along.

We start with a standard Adventr iframe embed and then add some divs to the HTML that will eventually display our questions and results (one div per question). They look like the following (with incrementing IDs (e.g. results_1, results_2, etc.):

<div class="results" id="results_1">
<h3 class="question"></h3>
<p class="correct-answer"><strong>A. Seven</strong></p>
<ul class="answers">
<li class="A">A <span class="count"></span></li>
<li class="B">B <span class="count"></span></li>
<li class="C">C <span class="count"></span></li>
<li class="D">D <span class="count"></span></li>
</ul>
</div>

We have CSS that ensures these divs are hidden until they're ready.

For this example, we are using Airtable for our results data, since it provides an easy-to-use API for storing and retrieving data. 


Note: For simplicity, we are accessing the Airtable API directly in javascript, however, this is not a recommended approach since it exposes our Airtable API Key. It would be better to handle the data retrieval and storage on your server-side so it's more secure.


We are also using jQuery for Ajax requests and DOM manipulation, but you can easily use other libraries if you prefer. 


Since we are only listening for user choices in the Adventr player, it's overkill to use the player.js library. Instead, we start with a simple listener so we can trigger events based on choices within the Adventr. The following code is all you need to listen for choices from the Adventr player and can trigger any behavior you might imagine:

// function to initialize listener:
function choiceListener() {
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
// first, determine if event is JSON
let received;
try {
received = JSON.parse(event.data);
} catch (e) {
received = {event: event.data}
}

// if the event is JSON, check that it's in the adventr format and if it's a choice event
if (received.context === 'adventr') {
if (received.event === 'choice') {
updateAirTable(received.value.id); // pass the choice's ID to our update function
}
}
}
}
// initialize the listener for choice events:
choiceListener();

You'll see that when we receive a postMessage that's in the right format, we pass the choice's ID to a function called updateAirTable(). That function handles most of the rest of the behavior. 


Before we define that function we define some variables we'll be using:

let airtable_data = {};
let airtable_key = 'YOURKEYHERE'; // Note that it's very insecure to directly use your airtable API Key in javascript like this. I'm only using this approach for this demo, and don't have any personal or other sensitive data in this table.

// The following hash defines how the Adventr Clip IDs relate to the questions and answers.
// Each clip's ID is based on its original filename. In my case, I have 'A2', 'A1', 'A1-1', and 'A1-2' as possible choices on the first question, and so forth.
let question_mapping = {
'A1-2': {question: 1, answer: 'D'},
'A1-1': {question: 1, answer: 'C'},
'A1': {question: 1, answer: 'B'},
'A2': {question: 1, answer: 'A'},

'B1-2': {question: 2, answer: 'D'},
'B1-1': {question: 2, answer: 'B'},
'B1': {question: 2, answer: 'C'},
'B2': {question: 2, answer: 'A'},

'C1-2': {question: 3, answer: 'D'},
'C2': {question: 3, answer: 'C'},
'C1-1': {question: 3, answer: 'B'},
'C1': {question: 3, answer: 'A'},

}

We also need a function that obtains the state of the Airtable data when the page loads. This function uses Ajax to request the Airtable API and then copies the data into our airtable_data variable.

function getAirTable() {
$.ajax ({
url: "https://api.airtable.com/v0/appDLZRNiYpkMsrgz/Table%201?maxRecords=3&view=Grid%20view&api_key="+airtable_key,
success: function (data, textStatus, jqXHR) {
console.log (data);
airtable_data = data['records'];
},
error: function () {
}
});
}

// I'm using jQuery to call this function after the page is fully loaded:
$( document ).ready(function() {
getAirTable();
});

Now, let's come back to our main function of updateAirTable(). Most of this function is handling the DOM manipulation to change the values and appearance of hidden HTML already on the page. Depending on your design, much of this could be very different.

I recommend viewing the source of the demo page to see the HTML that is being manipulated.

// this function accepts a choice ID as seen in the mapping above, e.g. "A1-1" etc. It's important that you ensure you are using the IDs used in your final published Adventr project.

function updateAirTable(choice) {
// first we make sure that the choice we received is one of the ones we are looking for in the mapping. This ensures that other navigation in your Adventr is ignored.
if (mapped_choice = question_mapping[choice]) {
// we add 1 to the number of people who have made this choice
airtable_data[mapped_choice['question']-1]['fields'][mapped_choice['answer']] += 1;
// I'm using jQuery to work on the div containing the relevant question
results_div = $('#results_'+mapped_choice['question']);

// now I'm updating the question from the airtable data
results_div.find(".question").html(airtable_data[mapped_choice['question']-1]['fields']['Name']);

// since I'm displaying the answers with a bar chart, I want to calculate my scale
max_answer = Math.max(airtable_data[mapped_choice['question']-1]['fields']['A'], airtable_data[mapped_choice['question']-1]['fields']['B'], airtable_data[mapped_choice['question']-1]['fields']['C'], airtable_data[mapped_choice['question']-1]['fields']['D']);

// Then I fill in the number of responses to each choice
results_div.find("li.A .count").html(airtable_data[mapped_choice['question']-1]['fields']['A']);
results_div.find("li.B .count").html(airtable_data[mapped_choice['question']-1]['fields']['B']);
results_div.find("li.C .count").html(airtable_data[mapped_choice['question']-1]['fields']['C']);
results_div.find("li.D .count").html(airtable_data[mapped_choice['question']-1]['fields']['D']);

// and I size each bar according to its proportion of answers
results_div.find("li.A").width(airtable_data[mapped_choice['question']-1]['fields']['A']/max_answer*100 +'%');
results_div.find("li.B").width(airtable_data[mapped_choice['question']-1]['fields']['B']/max_answer*100 +'%');
results_div.find("li.C").width(airtable_data[mapped_choice['question']-1]['fields']['C']/max_answer*100 +'%');
results_div.find("li.D").width(airtable_data[mapped_choice['question']-1]['fields']['D']/max_answer*100 +'%');


// I add classes to indicate the right answer and the answer that the viewer chose
results_div.find("li."+airtable_data[mapped_choice['question']-1]['fields']['Correct']).addClass("correct");
results_div.find("li."+mapped_choice['answer']).addClass("mine");
// and I'll show a message indicating if they were right or wrong
if (airtable_data[mapped_choice['question']-1]['fields']['Correct'] == mapped_choice['answer']) {
results_div.find(".correct-answer").prepend("Great job! You got it right: ");
}
else {
results_div.find(".correct-answer").prepend("<span class='incorrect'>Oops, wrong choice!</span> The correct answer was: ");
}

// finally I add a class that unhides the div
results_div.addClass('answered');

// And now I'm going to use jQuery Ajax to post the new results back to Airtable.
// In order to patch a table, you need to remove the createdTime read-only field.
delete airtable_data[mapped_choice['question']-1]['createdTime'];
$.ajax ({
url: "https://api.airtable.com/v0/appDLZRNiYpkMsrgz/Table%201?api_key="+airtable_key,
method: 'patch',
contentType: 'application/json',
data: JSON.stringify({records: [airtable_data[mapped_choice['question']-1]]}),
success: function (data, textStatus, jqXHR) {
console.log (data);
},
error: function () {
}
});

}
}

As you can see, the majority of the code here is modifying the DOM and customizing how we display the data. The actual listening for choice events and doing something with those events is fairly simple.


We'd love to see what you create with the API. Please post in the forums if you have something you want to show off!