I’m currently completing an Apprenticeship at a development agency in NYC called Stealthwerk. The team prides itself on being design minded and mobile-first, which is something I experience everyday in the way they discuss client deliverables including: design implementation, page load times, file structures and code reviews.

My most challenging deliverable yet involved implementing a carousel / slide of user generated images, hosted on Storify, for Tommy Hilfiger’s #MyTommyMag campaign. This was the first time I’d be integrating an API into a live project and there were a lot of things to consider - a few considerations I didn’t know I’d need to make until I started coding.

Screenshot - MyTommyMag

Below is a detailed outline of the process I went through, with code examples and explanations for what each chunk of code is doing at runtime.

  • The Goal: Pull in images from Tommy Hilfiger’s #MyTommyMag Storify page, along with the username and full name of each featured post. Then randomizing the order in which the images display within each UGC (user generated content) box.
  • How: (1) Use the Storify API to pull in images. (2) Figure out an algorithm to randomize the images.

Once I had a general outline of what needed to happen, then next step was figuring out how the Storify API worked and whether we’d be able to get the information we wanted from the #MyTommyMag Story.

Storify’s API documentation is quite lacking, more so when it comes to Stories, which is what I needed to work with. If you’re not familiar with Storify: A Storify ‘Story’ is the equivalent of a blog post / Pinterest board, it’s a page the user creates to display content of their choosing and it’s stored as a subset of their larger user landing page.

It took a bit of trial and error to figure out if we could get the information we wanted. Storify allows you to [Get a story (including elements)](http://dev.storify.com/api/stories#get but they don’t tell you elements are each sub-posting / sub-story for that page. The code example also doesn’t show you that inside elements the information you seek, will be found. All they give you is a short description:

If the story is private or not published, an error will be shown for unauthorized users. The elements array is paginated.

GET /stories/:username/:slug

Followed by an example of the object returned in a JSON request:

storify story code ex

For someone who’s just starting out, this is no help. Detail is provided about the other information that is accessible for a Story but "elements":[] has nothing. Also, there was little to be found when Googling. So, that left me with one option: Look at a test API call and figure out what’s where - much like I do when using Chrome dev tools to look at a websites DOM (Document Object Model).

This was painful as I was literally staring at a data dump that looks like this:

storify-testimonal-api

The next step was for to try and figure out what info is inside ‘elements’. As you can see, I did the best thing I knew how: hit command+f and queried the page for ‘elements’. I found them but it was still a cluster** of words and symbols.

Luckily I remembered reading a blog post / watching a Treehouse video that mentioned that if you add ?json to the end of an API request URL in your browser the data is reformatted to look a bit nicer. Doing this gave me:

storify-testimonal-json

You might not be able to tell the difference but when scanning the page it really does help, especially when you only kinda know what you’re looking for.

By reformatting to json, I was able to see a pattern that started: {"id":"4ea8e71b1f97f7106d00a7b5","eid"..}

I felt this was good enough to test drive on a live Tommy Hilfiger Story (instead of using the test page specified by Storify) and dive in deeper. As this was a work in progress, the Story we’d eventually want to query wasn’t live yet so I used a published Tommy Hilfiger story to get started on the API integration.

Based on the Storify documentation I put together my own API request url. For Storify, to send a GET request you use:

GET /stories/:username/:slug

Their example is telling you: This is what you want to use as inputs if you’re doing a GET request (because you’re GETting information) to a Story /stories/: and username/:slug is the information you need to tac onto the url so the request is made to the page and story you want. In the example they provide it’s:

http://api.storify.com/v1/stories/storify/testimonials

For testing purposes the story (I think of it as a post within on a blog) was The Conversation - #TommySpring16 - Mobile. The url for the page is: https://storify.com/TommyHilfiger/the-conversation-tommyspring16-mobile

To look at the API request in my browser I replaced the information where needed. Starting with the Storify example url below, I ended up with:

http://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile

And bam - show me the data!

tommy-story-json

I did another command+f to find ‘elements’ on the page and verified that I was on the right path.

totalElements is set to 6, which is the same number of images / sub-stories / sub-posts that show up on The Conversation Tommy Spring 16 Mobile page. Then, just to double-check, I read through the info following elements: and saw

javascript"type":"image","permalink":"https://twitter.com/wwd/status/643627832543830016"

I copy-pasted the url ( it’s after "permalink": ) into my browser and hit enter. Lone and behold, the image was the same as that for the 1st element on the Story page.

I continued scanning the array and found the username and name fields, along with a link to the image source. Knowing I could most likely grab the information we needed, I started coding.

This bit included setting up a storify-testpage.html file and adding tags for my JavaScript code. I knew where the API requests would get injected so I took bits of the #MyTommyMag html code and pasted it into storify-testpage.html. I wanted to make sure I was using the correct classes so that my Javascript code worked when moved into the main.js file. I also added this script to execute the API request:

var xmlhttp = new XMLHttpRequest(), method='GET', url='https://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile'; 

xmlhttp.open( method, url, true );

xmlhttp.onreadystatechange = function() {
    if ( 4 != xmlhttp.readyState ) {
        return;
    }
    if ( 200 != xmlhttp.status ) {
        return;
    }
    console.log( xmlhttp.responseText );
};

xmlhttp.send();

I committed the update and Will (my boss and the lead on this project) quickly removed the HTML I had pasted in, replacing it with:

testing storify api

< div id="storify" > < /div >

Will sent me a HipChat message explaining that Tommy Hilfiger hadn’t yet confirmed whether they wanted the images to be displayed as they were currently (a select number of images had been hardcoded) and that even though it was likely they’d choose to display the images on the current pages, it was best to keep everything separate for now. He suggested I start with a clean html page adding in only what I need for testing and focus on getting the API to work.

I agreed and started getting to work - First order: Get the information we want pulled in through the API request and appended inside

< div id='storify'> [here] < /div >

So I added a little more HTML markup and changed the API call so it was a $.getJSON() request. When I left work this is what I had:

< div class='site-main' >
    < div id='storify' > 
      < p >testing storify api< /p >
      < button >search< /button >
    < /div >
< /div >

< script type='text/javascript' >
  $(document).ready(function(){
  var url='https://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile’;
  $('button').click(function(){
     $.getJSON(url, function(json){
          //console.log(json);
       $.each(json.content, function(i, elements){
          // var data = elements.data;
          // var queryImg = data.image;
          // var source = elements.source;
          $('#storify').append('< p >'+elements.id+'< /p >'+'< p >'+elements.username+'< /p >');
        });
      });   
    });
  });
< /script >

The code below didn’t work.

This was the first time I was properly working with an API for a project. Prior to this I had worked with the Twitter, Crunchbase and Weather.com API’s but hadn’t done much. For Twitter I got set up with an API key and read through the documentation, wrote a little pseudocode but didn’t make any API requests outside their dev tool. Crunchbase was similar but I was also using Backbone and Marionette so that was another learning curve. With Weather.com I got the request working but didn’t do anything with it. This was done during a Front End 101 class I had taken months ago and I only vaguely remembered it (if only I’d written a blog post about it..).

So, I did the thing most people do. I Googled “JSON API request” and found the syntax that looked closest to what I remembered doing before and started testing to make sure I was grabbing the correct information.

The console.log(json) was added in to test whether the json request was working, which it was because the json object was being loaded in (I could see it in the console).

This meant the issue was most likely occurring in the $.each() loop. I started testing different things, commenting out (“//”) as I went along – this is why some things are commented out.

I was getting the error: Uncaught TypeError: Cannot read property 'image' of undefined. Which I understood as translating to: Hey you, you’re telling to find image, but I’m not finding it under elements.data.

You’d think understanding the error message is enough but in reality, it’s not. Having an idea of what the error message meant, didn’t help me understand why I was getting the error.

As far as I knew, this should’ve worked. Using console.log(json) I saw image was nested under elements -> data, so something was off.. but what?!

The issue with this code is that I’m referencing nearly all the wrong things! What you see versus what you’re actually doing in your code can vary a lot and it doesn’t take much - key learning here.

I didn’t get a chance to solve this 100% on my own because I had a deadline so I sought help after work and did a little pair programming with a friend. We’ll get to that in a bit..

Here’s an explanation of why the code I wrote initially doesn’t work

Since - because of deadlines - I didn’t get a chance to explore why my code wasn’t working, I went back and figured it out once the project was done.

So that you can understand why as well, I’m going to show you what a functional version of the code I initially wrote looks like and explain why the code I wrote didn’t work.

This is what the script should’ve been (the bold text is what needed to be added in):

$(document).ready(function(){

  var url="https://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile";

  $("button").click(function(){
    $.getJSON(url, function(json){
      $.each(json.content.< strong >elements< /strong >, function(i, elements){
        $('#storify').append("< p >'+elements.id+'< /p >'+'< p >'+elements.< strong >attribution< /strong >.username+'< /p >');
      });
    });   
  });
});

The $.each() loop can use either an obj or an array to iterate over, and uses one of the following syntax:

when using an object:

$.each( obj, function( key, value ) { 
 //whatever code you want to run next 
});

when using an array:

$.each( arr, function( i, val ) { 
 //whatever code you want to run next 
});

Using debugger; to test, I discovered many things

I was using the Object variation of $.each() and was setting obj to json.content. So i(key) was running through each key in the object, with elements being set equal to each corresponding value. My ( key , value ) was ( i, elements ). For the first run through the loop, the key:value pair was "sid":"55e83ce03e694a643316dfbf".

This means that when I was referencing elements.id and getting the error Uncaught TypeError: Cannot read property 'image' of undefined. It was because elements was the value "55e83ce03e694a643316dfbf", so what I was actually asking for was: "55e83ce03e694a643316dfbf".id which doesn’t exist making it ‘undefined’.

object query one

The second time through the loop the key:value pair is "title": "The Conversation - #TommySpring16 - Mobile". So when I tried retrieving elements.username I was actually telling the computer I wanted "The Conversation - #TommySpring16 - Mobile".username.

object query two

Not at all what I thought I was doing. Mind blown, seriously. Human to Computer speech, line-by-line needs to be verified for correct interpretation. Otherwise, headaches and error logs.

Earlier in Sept I went to a GoBridge workshop and Bill Kennedy, our instructor, stated that for every line of code one writes, there are at least 3 bugs - You might not always find them, but they exist. With this experience I proved to myself that it’s true. In this one line of code: $.each(json.content, function(i, elements){

I created 3 bugs…

  • Referencing an Object instead of an Array
  • i refers to a key
  • elements refers to a value

I made one decision and it trickled to the rest of my code. And honestly, I’d have been racking my brain if not for the debugger tool which allows you to step through the code you’ve written line by line (you can do this by selecting breakpoints in Chrome Dev Tools as well). It’s holy, really and truly! Use it. You’ll fall in love.

If you use Chrome here’s a good resource for discovering all sorts of ways you can debug JavaScript: https://developer.chrome.com/devtools/docs/javascript-debugging

So having discovered I was inputting the wrong information, I rewrote the $.each() loop. In the updated version I’m referencing an Array: json.content.elements, making i an iterator that starts at 0 by default; value remains elements. In this instance, the Array has length 6 so i will be incremented until it’s run all 6 times and each time through the loop, elements will be updated to reference the Elements Object for that index:

array query

That’s one bug down. Onto the next.

Once $.each() was running correctly, I was getting elements.id but elements.username was still returning an error. Since using console.log(json) shows you the full JSON object in the console, I was able to go down the object tree and find username. Turned out it’s located in two places: source and attribution. For this example I used attribution, giving me: elements.attribution.username. Inspecting the JSON object is far easier on the eyes (example below) when using the Console in Chrome dev tools (highly suggest you get comfy with it).

console log tommy

Back to the process I went through to get my project deliverable done in time

Because of deadlines I didn’t have much time to explore why my code wasn’t working, so I sought help from a friend. He walked me through the code as he would have done it and we came up with the following (this code was also refactored so it’s easier to read):

var fetchImages = function() {
  var url="https://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile";
  $.getJSON(url, insertImage);
}

var insertImage = function(result){
  for (var i in result.content.elements) {
    var element = result.content.elements[i];
    var username = element.source.username;
    var full_name = element.attribution.name;
    var image_url = element.data.image.src;
    $('#storify').append("< p >"+"< img src='"+image_url+"' >"+"< /p >"+"< p >"+full_name+"  @"+username+"< /span >");
  }
}

$(function(){
  $("button").click(fetchImages);
});

In this version we’re using the JavaScript for-in loop instead of jQuery’s $.each(). Two functions were also created, one for the $.getJSON request and another for the loop, thus separating the API call from the action you want to have happen once the request comes through.

Once that was working I added in an if() statement to grab the last 4 images, as grabbing only a subset of all Story elements was added into the specification for this project and removed the button so images showed up once the page was loaded. This looked like:

var fetchImages = function() {
  var url="https://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile";
  $.getJSON(url, insertImage);
}

var insertImage = function(result){
  //debugger;
  for (i in result.content.elements) {
    if(i > result.content.elements.length-4) {
      var element = result.content.elements[i];
      //info being retrieved
      var username = element.source.username;
      var full_name = element.attribution.name;
      var user_profile_url = element.attribution.href;
      var image_url = element.data.image.src;
      var image_caption = element.data.image.caption;
      var permalink = element.permalink;
      var posted_at = element.posted_at;
      $('#storify').append(
        "< p >"+"< img src='"+image_url+"' >"+"< /p >"+
        "< p > Posted by "+full_name+"  @"+username+"< /p >"+
        "< p > User Profile - < a href='http:"+user_profile_url+"' > Link to user profile < /a >< /p >"+
        "< p > Caption - @"+image_caption+"< /p >"+
        "< p > Permalink - "+permalink+"< /p >"+
        "< p > Posted "+posted_at+"< /p >");
    }
  }
}

$(function(){
  fetchImages();
});

The next day at work, came the code review. This is what the code looked like after that:

var fetchImages = function() {
  var url="https://api.storify.com/v1/stories/TommyHilfiger/the-conversation-tommyspring16-mobile?&api_key=[key-here]";
  $.getJSON(url, insertImage);
};

var insertImage = function(result){
  var stories = [];
  var counter = 0;
  for(i = result.content.elements.length-4; i < result.content.elements.length; i++){
    stories[counter] = {};
    var element = result.content.elements[i];
    stories[counter]['username'] = element.source.username;
    stories[counter]['full_name'] = element.attribution.name;
    stories[counter]['user_profile_url'] = element.attribution.href;
    stories[counter]['image_url'] = element.data.image.src;
    stories[counter]['image_caption'] = element.data.image.caption;
    stories[counter]['permalink'] = element.permalink;
    stories[counter]['posted_at'] = element.posted_at;
    counter++;
  };
  stories = shuffle(stories);
  $('.box-one .story').each(function(i){
    $(this).append(
      "< p >< img src='"+stories[i]['image_url']+"' style='width:200px; height:200px' >"+"< /br >"+
      "Posted by "+stories[i]['full_name']+"  @"+stories[i]['username']+"< /br >"+
      "User Profile - < a href='http:"+stories[i]['user_profile_url']+"' > Link to user profile < /a >< /br >"+
      "Caption - @"+stories[i]['image_caption']+"< /br >"+
      "Permalink - "+stories[i]['permalink']+"< /br >"+
      "Posted "+stories[i]['posted_at']+"< /p >"
    );
  });
  $('.box-two .story').each(function(i){
    i = i+1; //increases i by 1 so images load in different sequence (ahead by 1)
    $(this).append(
      "< p >< img src='"+stories[i]['image_url']+"' style='width:200px; height:200px' >"+"< /br >"+
      "Posted by "+stories[i]['full_name']+"  @"+stories[i]['username']+"< /br >"+
      "User Profile - < a href='http:"+stories[i]['user_profile_url']+"' > Link to user profile < /a >< /br >"+
      "Caption - @"+stories[i]['image_caption']+"< /br >"+
      "Permalink - "+stories[i]['permalink']+"< /br >"+
      "Posted "+stories[i]['posted_at']+"< /p >"
    );
  });
};
function shuffle(array) {

var currentIndex = array.length, temporaryValue, randomIndex ;
// While there remain elements to shuffle...

while (0 !== currentIndex) {
  // Pick a remaining element...
  randomIndex = Math.floor(Math.random() * currentIndex);
  currentIndex -= 1;

  // And swap it with the current element.
  temporaryValue = array[currentIndex];
  array[currentIndex] = array[randomIndex];
  array[randomIndex] = temporaryValue;
  };
  return array;
};

$(function(){ fetchImages(); });

The code was changed quiet a bit, but it happened in a couple iterations.

  • During the code review: My teammate, Shahruk, suggested I use a for loop instead of the for-in loop, which cuts down on a line of code by removing the if statement.

  • At the end of the code review my teammate asked what I was working on at that moment and what I needed to do next to finish the integration. I told him I was looking into randomizing i because the client wanted the images to display inside 3 different boxes on one webpage and wanted them to show up in different order so the user isn’t seeing the same images in all boxes.

Shahruk told me about an algorithm he’d learned about at school -> Fisher–Yates shuffle jazz hands and suggested I search for “Fisher-Yates shuffle javascript” in Google.

Google led us to: http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array (code is below):

function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex ;
  // While there remain elements to shuffle...
  
  while (0 !== currentIndex) {
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
  
    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }
  return array;
}

Shahruk then helped me integrate it into the codebase and bam, randomization worked!

As the shuffle function takes in an array as it’s parameter, we had to change the code around a bit because the for loop returns an object.

To accomplish this we created an array for shuffle() to process: var stories = [];

Inside the for loop we initialized an empty Object – stories[counter] = {}; – to store each key:value pairing for the elements we want to store. This object is then added into the stories array.

Digging deeper

The way the code read didn’t make it clear why counter and stories[counter] were being used, I thought I might be missing something so I checked with a friend (whilst writing this blog post) and it turns out they’re not needed! This bit of code could have also been written as:

var stories = [];
for(i = result.content.elements.length-4; i < result.content.elements.length; i++){
  var element = result.content.elements[i];
  stories.push({
    username: element.source.username,
    full_name: element.attribution.name,
    user_profile_url: element.attribution.href,
    image_url: element.data.image.src,
    image_caption: element.data.image.caption,
    permalink: element.permalink,
    posted_at: element.posted_at
});

var counter and the stories[counter] preceding each key instance isn’t needed. I’m assuming this choice came down to personal preference.

What this bit of code does (referencing the original code sample):

var insertImage = function(result){
  var stories = [];
  var counter = 0;
  
  for(i = result.content.elements.length-4; i < result.content.elements.length; i++){
    stories[counter] = {};
    var element = result.content.elements[i];
    stories[counter]['username'] = element.source.username;
    stories[counter]['full_name'] = element.attribution.name;
    stories[counter]['user_profile_url'] = element.attribution.href;
    stories[counter]['image_url'] = element.data.image.src;
    stories[counter]['image_caption'] = element.data.image.caption;
    stories[counter]['permalink'] = element.permalink;
    stories[counter]['posted_at'] = element.posted_at;
    counter++;
  };  
  
  stories = shuffle(stories);

The stories array gets passed into shuffle once the for loop completes and inside it are stories[counter] Objects (4 in this case, because the loop runs 4 times given i). Each stories[counter] object has a key:value pairing of the information we’ve stated we want (ex: stories[counter]['username'] = element.source.username;).

When stories is passed into shuffle(), all array elements are shuffled and returned. Each stories array element (in this case the stories[counter] object) is then inserted into $('.box-one .story') or $('.box-two .story') by using $.each(function(i)){} (which loops through each array element, based on i) and calling $(this).append() inside that function.

As you might’ve noticed, I’m increasing i by 1 for $('.box-two .story'). I did this so the images would be loaded in 1 ahead of $('.box-one .story) so that a user wouldn’t see the same image at the same time. This sounded like it could work in theory but in implementation, it didn’t. Why didn’t it work?

Because the images that had i being offset by 1, ended up with a blank slide at the end of the loop because the div elements for each slide where the images were being injected was hardcoded. For 4 div elements, the loop (when offset by 1) was only grabbing 3 items. The 1st image was also being skipped so instead of 4 images, we were only grabbing 3.

With offsetting i not being the right solution, I thought - Hey, how about we just make an API call 3 times and then pass the stories array for each call into the shuffle function. This way, the shuffle function is getting the same API data but the query is being saved into different variables running their own function and saving a stories array independently of the others (even though the code inside the function is identical). It looked like this:

// Storify API integration
var fetchImages = function() {
  var url="https://api.storify.com/v1/stories/TommyHilfiger/mytommymag?&api_key=[key-here]";
  $.getJSON(url, insertImage);
  $.getJSON(url, insertImage_MTM_box1);
  $.getJSON(url, insertImage_MTM_box2);
  $.getJSON(url, insertImage_MTM_box3);
};

var insertImage = function(result){
  var stories = [];
  var counter = 0;

  for(i = result.content.elements.length-20; i <  result.content.elements.length; i++){
    stories[counter] = {};
    var element = result.content.elements[i];
    stories[counter]['image_url'] = element.data.image.src;
    stories[counter]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      stories[counter]['username'] = element.source.username;
      } else {
        stories[counter]['username'] = element.meta.author_name;
    }
    // stories[counter]['user_profile_url'] = element.attribution.href;
    // stories[counter]['image_caption'] = element.data.image.caption;
    // stories[counter]['permalink'] = element.permalink;
    // stories[counter]['posted_at'] = element.posted_at;
    counter++;
  };
  stories = shuffle(stories);
  // needs work
  if($(".mod-fashion-grid").length) { 
    $('.ugc').each(function(i){
      $(this).addClass('mod-fashion-grid-social__slide--ugc--'+[i]);
      $(this).css("background-image", "url('" + stories[i]['image_url'] + "')");
      $(this).append(
        "< div class='mod-fashion-grid-social__content' >"+
          "< h2 class='mod-fashion-grid-social__headline mod-fashion-grid-social__headline--white'>#MyTommyMag< /h2 >"+
          "< div class='mod-fashion-grid-social__copy mod-fashion-grid-social__copy--white' >< span class='span-text span-text--1' >Show us your style!< /span >< /div >"+
          "< div class='mod-fashion-grid-social__cta mod-fashion-grid-social__cta--white' >Learn More < span class='icon icon-right-arrow' >< /span >< /div >"+
        "< /div >"
      );
    });
  };
  // needs work
  if($(".mod-home-grid").length) { 
    $('.mod-home-grid__item-slide--ugc').each(function(i){
      $(this).addClass('mod-home-grid__item-slide--'+[i]);
      $(this).css("background-image", "url('" + stories[i]['image_url'] + "')");
      $(this).append(
        "< div class='mod-home-grid__content' >" +
          "< h2 class='mod-home-grid__headline' >#MyTommyMag< /h2 >" +
          "< div class='mod-home-grid__copy' >< span class='span-text span-text--1' >Show us your style!< /span >< /div >"+
          "< div class='mod-home-grid__cta' >Learn More < span class='icon icon-right-arrow' >< /span >< /div >"+
        "< /div >"
      );
    });
  };
};
// MY TOMMY MAG 
var insertImage_MTM_box1 = function(result){
  var stories = [];
  var counter = 0;
  for(i = result.content.elements.length-20; i <  result.content.elements.length; i++){
    stories[counter] = {};
    var element = result.content.elements[i];
    stories[counter]['image_url'] = element.data.image.src;
    stories[counter]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      stories[counter]['username'] = element.source.username;
      } else {
        stories[counter]['username'] = element.meta.author_name;
    }
    // stories[counter]['user_profile_url'] = element.attribution.href;
    // stories[counter]['image_caption'] = element.data.image.caption;
    // stories[counter]['permalink'] = element.permalink;
    // stories[counter]['posted_at'] = element.posted_at;
    counter++;
  };
  stories = shuffle(stories);
  if($(".mytommymag-grid").length) { 
    $('.mytommymag-grid__image-wrapper .ugc-box-1').each(function(i){
      $(this).addClass('mytommymag-grid__item-slide--'+[i]);
      $(this).append(
        "< img src='"+stories[i]['image_url']+"'>"+
        "< span class='mytommymag-grid__cta' >"+stories[i]['full_name']+"  @"+stories[i]['username']+"< /span >< /div >"
      );
    });
  };
};
var insertImage_MTM_box2 = function(result){
  var stories = [];
  var counter = 0;
  for(i = result.content.elements.length-20; i <  result.content.elements.length; i++){
    stories[counter] = {};
    var element = result.content.elements[i];
    stories[counter]['image_url'] = element.data.image.src;
    stories[counter]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      stories[counter]['username'] = element.source.username;
      } else {
        stories[counter]['username'] = element.meta.author_name;
    }
    // stories[counter]['user_profile_url'] = element.attribution.href;
    // stories[counter]['image_caption'] = element.data.image.caption;
    // stories[counter]['permalink'] = element.permalink;
    // stories[counter]['posted_at'] = element.posted_at;
    counter++;
  };
  stories = shuffle(stories);
  if($(".mytommymag-grid").length) { 
    $('.mytommymag-grid__image-wrapper .ugc-box-2').each(function(i){
      $(this).addClass('mytommymag-grid__item-slide--'+[i]);
      // i = i+2; //increases i by 1 so images load in different sequence (ahead by 2)
      $(this).append(
        "< img src='"+stories[i]['image_url']+"' >"+
        "< span class='mytommymag-grid__cta' >"+stories[i]['full_name']+"  @"+stories[i]['username']+"< /span >< /div >"
      );
    });
  };
};
var insertImage_MTM_box3 = function(result){
  var stories = [];
  var counter = 0;
  for(i = result.content.elements.length-20; i <  result.content.elements.length; i++){
    stories[counter] = {};
    var element = result.content.elements[i];
    stories[counter]['image_url'] = element.data.image.src;
    stories[counter]['full_name'] = element.attribution.name; 
    if (element.source.name === 'twitter') {
      stories[counter]['username'] = element.source.username;
      } else {
        stories[counter]['username'] = element.meta.author_name;
    }
    // stories[counter]['user_profile_url'] = element.attribution.href;
    // stories[counter]['image_caption'] = element.data.image.caption;
    // stories[counter]['permalink'] = element.permalink;
    // stories[counter]['posted_at'] = element.posted_at;
    counter++;
  };
  stories = shuffle(stories);
  if($(".mytommymag-grid").length) { 
    $('.mytommymag-grid__image-wrapper .ugc-box-3').each(function(i){
      $(this).addClass('mytommymag-grid__item-slide--'+[i]);
      // i = i+2; //increases i by 1 so images load in different sequence (ahead by 1)
      $(this).append(
        "< img src='"+stories[i]['image_url']+"'>"+
        "< span class='mytommymag-grid__cta'>;"+stories[i]['full_name']+"  @"+stories[i]['username']+"< /span >< /div >"
      );
    });
  };
};
function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;
  // While there remain elements to shuffle...
  
  while (0 !== currentIndex) {
  
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
  
    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  };
  return array;
};

$(function(){ fetchImages(); });

This worked BUT it also meant I was making the API request 4 times. I knew it wasn’t ideal but at that moment I was looking for the quickest solution and not the best one. It wasn’t optimal so back to the chopping board.

Also you might’ve noticed reading through the function that fires on a successful API request, that I added in an if() statement to check for the source name. For this Storify story, the images were either being pulled in from Twitter or Instagram (aka ‘source’) and the username was stored under a different element.

I discovered this bug because as the images were being shown on the site, some returned ‘undefined’ for the username, Twitter was working but Instagram wasn’t. The if statement below checks for the value of element.source.name, if it’s equal to Twitter is queries for element.source.username - If it’s not equal to Twitter, than it goes to element.meta.author_name to grab the info I want. This could have been written in either one of the following ways, I just happened to set it to Twitter:

Validate for Twitter:

if (element.source.name === "twitter") {
  ugcImages_box3[counter3]["username"] = element.source.username;
  } else {
    ugcImages_box3[counter3]["username"] = element.meta.author_name;
}

Validate for Instagram:

if (element.source.name === "instagram") {
  ugcImages_box3[counter3]["username"] = element.meta.author_name;
  } else {
    ugcImages_box3[counter3]["username"] = element.source.username;
}

To get the API requests down I thought about the best solution given what we wanted, API limits and traffic to the site. The best option I came up with was: Creating 3 separate variables to store a stories array once the shuffle function has completed. This is similar to my thought process in running the API request multiple times but this way we make only 2 requests. I created 3 variables for the MyTommyMag page (one per blue box). This worked but we’re still making 2 API requests (want to get it down to 1):

// Storify API integration
var fetchImages = function(){ 
  var url='https://api.storify.com/v1/stories/TommyHilfiger/mytommymag?&api_key=[key-here]';
  $.getJSON(url, insertImage);
  $.getJSON(url, insertImage_MTM);
};

var latestImages = 20;

// Images for Homepage, Fashion Mods
var insertImage = function(result){
  var ugcImages = [];
  var counter = 0;

  for(i = result.content.elements.length-latestImages; i <  result.content.elements.length; i++){
    ugcImages[counter] = {};
    var element = result.content.elements[i];
    ugcImages[counter]['image_url'] = element.data.image.src;
    ugcImages[counter]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      ugcImages[counter]['username'] = element.source.username;
      } else {
        ugcImages[counter]['username'] = element.meta.author_name;
    }
    counter++;
  };

  ugcImages = shuffle(ugcImages);
  if($(".mod-home-grid").length) { 
    $('.mod-home-grid__item-slide--ugc').each(function(i){
      $(this).addClass('mod-home-grid__item-slide--'+[i]);
      $(this).css("background-image", "url('"+ugcImages[i]['image_url']+"')");
      $(this).append(
        "< div class='mod-home-grid__content' >"+
          "< h2 class='mod-home-grid__headline' >#MyTommyMag< /h2 >"+
          "< div class='mod-home-grid__copy' >< span class='span-text span-text--1' >Show us your style!< /span >< /div >"+
          "< div class='mod-home-grid__cta' >Learn More < span class='icon icon-right-arrow' >< /span >< /div >"+
        "< /div >"
      );
    });
  };
  if($(".mod-fashion-grid").length) { 
    $('.ugc').each(function(i){
      $(this).addClass('mod-fashion-grid-social__slide--ugc--'+[i]);
      $(this).css("background-image", "url('"+ugcImages[i]['image_url']+"')");
      $(this).append(
        "< div class='mod-fashion-grid-social__content' >"+
          "< h2 class='mod-fashion-grid-social__headline mod-fashion-grid-social__headline--white' >#MyTommyMag< /h2 >"+
          "< div class='mod-fashion-grid-social__copy mod-fashion-grid-social__copy--white' >< span class='span-text span-text--1' >Show us your style!< /span >< /div >"+
          "< div class='mod-fashion-grid-social__cta mod-fashion-grid-social__cta--white' >Learn More < span class='icon icon-right-arrow' >< /span >< /div >"+
        "< /div >"
      );
    });
  };
};

// Images for MyTommyMag UGC Boxes
var insertImage_MTM = function(result){
  var ugcImages_box1 = [];
  var ugcImages_box2 = [];
  var ugcImages_box3 = [];
  var counter1 = 0;
  var counter2 = 0;
  var counter3 = 0;

  //Box 1
  for(i = result.content.elements.length-latestImages; i <  result.content.elements.length; i++){
    ugcImages_box1[counter1] = {};
    var element = result.content.elements[i];
    ugcImages_box1[counter1]['image_url'] = element.data.image.src;
    ugcImages_box1[counter1]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      ugcImages_box1[counter1]['username'] = element.source.username;
      } else {
        ugcImages_box1[counter1]['username'] = element.meta.author_name;
    }
    counter1++;
  };
  ugcImages_box1 = shuffle(ugcImages_box1);
  if($(".mytommymag-grid").length) { 
    $('.mytommymag-grid__image-wrapper .ugc-box-1').each(function(i){
      $(this).addClass('mytommymag-grid__item-slide--'+[i]);
      $(this).append(
        "< img src='"+ugcImages_box1[i]['image_url']+"' >"+
        "< span class='mytommymag-grid__cta' >"+ugcImages_box1[i]['full_name']+"  @"+ugcImages_box1[i]['username']+"< /span >"
      );
    });
  };

  //Box 2
  for(i = result.content.elements.length-latestImages; i <  result.content.elements.length; i++){
    ugcImages_box2[counter2] = {};
    var element = result.content.elements[i];
    ugcImages_box2[counter2]['image_url'] = element.data.image.src;
    ugcImages_box2[counter2]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      ugcImages_box2[counter2]['username'] = element.source.username;
      } else {
        ugcImages_box2[counter2]['username'] = element.meta.author_name;
    }
    counter2++;
  };
  ugcImages_box2 = shuffle(ugcImages_box2);
  if($(".mytommymag-grid").length) { 
    $('.mytommymag-grid__image-wrapper .ugc-box-2').each(function(i){
      $(this).addClass('mytommymag-grid__item-slide--'+[i]);
      $(this).append(
        "< img src='"+ugcImages_box2[i]['image_url']+"' >"+
        "< span class='mytommymag-grid__cta' >"+ugcImages_box2[i]['full_name']+"  @"+ugcImages_box2[i]['username']+"< /span >"
      );
    });
  };

  //Box 3
  for(i = result.content.elements.length-latestImages; i <  result.content.elements.length; i++){
    ugcImages_box3[counter3] = {};
    var element = result.content.elements[i];
    ugcImages_box3[counter3]['image_url'] = element.data.image.src;
    ugcImages_box3[counter3]['full_name'] = element.attribution.name;
    if (element.source.name === 'twitter') {
      ugcImages_box3[counter3]['username'] = element.source.username;
      } else {
        ugcImages_box3[counter3]['username'] = element.meta.author_name;
    }
    counter3++;
  };
  ugcImages_box3 = shuffle(ugcImages_box3);
  if($(".mytommymag-grid").length) { 
    $('.mytommymag-grid__image-wrapper .ugc-box-3').each(function(i){
      $(this).addClass('mytommymag-grid__item-slide--'+[i]);
      $(this).append(
        "< img src='"+ugcImages_box3[i]['image_url']+"' >"+
        "< span class='mytommymag-grid__cta' >"+ugcImages_box3[i]['full_name']+"  @"+ugcImages_box3[i]['username']+"< /span >"
      );
    });
  };
};

function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;
  // While there remain elements to shuffle...

  while (0 !== currentIndex) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;

  };
  return array;
};

$(function(){ fetchImages(); });

Verifying that this worked, I tried cutting down on the API request again.

To get the API request down to 1, I refactored the code so all array variables and counters were instantiated outside function(result){} so that this function could be called once, being assigned to the variable initiated in the ` $.getJSON(url, variable) ` API call.

Whilst working on this, I also started getting a same-origin error and so had to convert the API request from JSON to JSONP. The same-origin error occurs because when making an API request on the front-end you’re using one website (www.yoursite.com) to request / GET information from another site (www.siteyouwant.com) and they have different domains. Adding a callback in the request, solves this. The callback is what differentiates a JSONP request from a JSON one.

This is the update version of the new AJAX / JSONP request:

// Storify API integration
var latestImages = 20;
var ugcImages = []; // Homepage & Fashion Mods
var counter = 0;
var ugcImages_box1 = []; // MyTommyMag
var counter1 = 0;
var ugcImages_box2 = []; // MyTommyMag
var counter2 = 0;
var ugcImages_box3 = []; // MyTommyMag
var counter3 = 0;

// API call
$.ajax({
  dataType: 'jsonp',
  url: 'https://api.storify.com/v1/stories/TommyHilfiger/mytommymag?&api_key=[key-here]',
  jsonp: 'callback',
  data: {
    q: 'ugcImages',
    format: 'json'
  },
  success: function(result){
    // Homepage & Fashion Mods  
    for(i = result.content.elements.length-latestImages; i <  result.content.elements.length; i++){
      ugcImages[counter] = {};
      var element = result.content.elements[i];
      ugcImages[counter]['image_url'] = element.data.image.src;
      ugcImages[counter]['full_name'] = element.attribution.name;
      if (element.source.name === 'twitter') {
        ugcImages[counter]['username'] = element.source.username;
        } else {
          ugcImages[counter]['username'] = element.meta.author_name;
      }
      counter++;
    };

    ugcImages = shuffle(ugcImages);

    [++ same markup as previous example]

Once I had this synced up it was time for code review and more testing.

Whilst testing rate limits, we found a bug. When the rate limit was hit and the API request failed, a series of empty blue boxes were being cycled through. Why? Because the div elements were hard coded inside the HTML file for the page and so when the API request failed, the images didn’t load but the div’s still did. The solution here was to add the div elements in dynamically. We’d also have to add in fail safes for when the API requests failed for other reasons, like timeouts.

The project was due the following morning, so I wouldn’t have enough time to test things out. It was time for pair programming to get this done as quickly as possible. Shahruk worked on this with me, and Will did a final code review. The following code is what we delivered to Tommy at the end:

// Storify API integration
var TM_latestImages = 20;
var TM_ugcImages = []; // Homepage & Fashion Mods
var TM_counter = 0;
var TM_ugcImages_box1 = []; // MyTommyMag
var TM_counter1 = 0;
var TM_ugcImages_box2 = []; // MyTommyMag
var TM_counter2 = 0;
var TM_ugcImages_box3 = []; // MyTommyMag
var TM_counter3 = 0;
var TM_startAjaxCycle = function () {
  $(".js-cycle-ajax").cycle({
    "speed": 1000
  });
};
$.ajax({
  dataType: 'jsonp',
  url: 'https://api.storify.com/v1/stories/TommyHilfiger/mytommymag?&api_key=[key-here]',
  jsonp: 'callback',
  async: true,
  timeout: 5000,
  data: {
    q: 'TM_ugcImages',
    format: 'json'
  }
}).done(function(result, textStatus, xhr){
  if(result.content) {
    $(".js-cycle-ajax").each(function(i) {
      var divNumber = i+1; // Because i starts at 0
      var html = " >div class='mytommymag-grid__image-wrapper ugc-box-"+divNumber+"' >< /div >";
      // Change the HTML based on which block this is
      if($(this).hasClass("js-cycle-ajax--fashion")) {
          html = " >div class='mod-fashion-grid-social__slide ugc' >< /div >";
        } else if($(this).hasClass("js-cycle-ajax--homepage")) {
          html = "< div class='mod-home-grid__item-slide mod-home-grid__item-slide--ugc mod-home-grid__item-slide--light' >< /div >";
        }
      // Add our HTML 20 times to this js-cycle-ajax block
      for(var i = 0; i <  TM_latestImages; i++) {
        $(this).append(html);
      }
    });
    // Homepage & Fashion Mods  
    for(i = result.content.elements.length-TM_latestImages; i <  result.content.elements.length; i++){
      TM_ugcImages[TM_counter] = {};
      var element = result.content.elements[i];
      TM_ugcImages[TM_counter]['image_url'] = element.data.image.src;
      TM_ugcImages[TM_counter]['full_name'] = element.attribution.name;
      if (element.source.name === 'twitter') {
        TM_ugcImages[TM_counter]['username'] = element.source.username;
        } else {
          TM_ugcImages[TM_counter]['username'] = element.meta.author_name;
      }
      TM_counter++;
    }
    TM_ugcImages = TM_shuffle(TM_ugcImages);
    if($(".mod-home-grid").length) { 
      $('.mod-home-grid__item-slide--ugc').each(function(i){
        $(this).addClass('mod-home-grid__item-slide--'+[i]);
        $(this).css("background-image", "url('"+TM_ugcImages[i]['image_url']+"')");
        $(this).append(
          "< div class='mod-home-grid__content' >"+
            "< h2 class='mod-home-grid__headline' >#MyTommyMag< /h2 >"+
            "< div class='mod-home-grid__copy' >< span class='span-text span-text--1' >Show us your style!< /span >< /div >"+
            "< div class='mod-home-grid__cta' >Learn More < span class='icon icon-right-arrow' >< /span >< /div >"+
          "< /div >"
        );
      });
    }
    if($(".mod-fashion-grid").length) { 
      $('.ugc').each(function(i){
        $(this).addClass('mod-fashion-grid-social__slide--ugc--'+[i]);
        $(this).css("background-image", "url('"+TM_ugcImages[i]['image_url']+"')");
        $(this).append(
          "< div class='mod-fashion-grid-social__content' >"+
            "< h2 class='mod-fashion-grid-social__headline mod-fashion-grid-social__headline--white' > #MyTommyMag< /h2 >"+
            "< div class='mod-fashion-grid-social__copy mod-fashion-grid-social__copy--white' >< span class='span-text span-text--1' >Show us your style!< /span >< /div >"+
            "< div class='mod-fashion-grid-social__cta mod-fashion-grid-social__cta--white' >Learn More < span class='icon icon-right-arrow' >< /span >< /div >"+
          "< /div >"
        );
      });
    }
    // MyTommyMag Box 1
    for(i = result.content.elements.length-TM_latestImages; i <  result.content.elements.length; i++){
      TM_ugcImages_box1[TM_counter1] = {};
      var element = result.content.elements[i];
      TM_ugcImages_box1[TM_counter1]['image_url'] = element.data.image.src;
      TM_ugcImages_box1[TM_counter1]['full_name'] = element.attribution.name;
      if (element.source.name === 'twitter') {
        TM_ugcImages_box1[TM_counter1]['username'] = element.source.username;
        } else {
          TM_ugcImages_box1[TM_counter1]['username'] = element.meta.author_name;
      }
      TM_counter1++;
    }
    TM_ugcImages_box1 = TM_shuffle(TM_ugcImages_box1);
    if($(".mytommymag-grid").length) { 
      $('.mytommymag-grid__image-wrapper .ugc-box-1').each(function(i){
        $(this).addClass('mytommymag-grid__item-slide--'+[i]);
        $(this).append(
          "< img src='"+TM_ugcImages_box1[i]['image_url']+"'/ >"+
          "< span class='mytommymag-grid__cta' >"+TM_ugcImages_box1[i]['full_name']+"  @"+TM_ugcImages_box1[i]['username']+"< /span >"
        );
      });
    }
    // MyTommyMag Box 2
    for(i = result.content.elements.length-TM_latestImages; i <  result.content.elements.length; i++){
      TM_ugcImages_box2[TM_counter2] = {};
      var element = result.content.elements[i];
      TM_ugcImages_box2[TM_counter2]['image_url'] = element.data.image.src;
      TM_ugcImages_box2[TM_counter2]['full_name'] = element.attribution.name; 
      if (element.source.name === 'twitter') {
        TM_ugcImages_box2[TM_counter2]['username'] = element.source.username;
        } else {
          TM_ugcImages_box2[TM_counter2]['username'] = element.meta.author_name;
      }
      TM_counter2++;
    }
    TM_ugcImages_box2 = TM_shuffle(TM_ugcImages_box2);
    if($(".mytommymag-grid").length) { 
      $('.mytommymag-grid__image-wrapper .ugc-box-2').each(function(i){
        $(this).addClass('mytommymag-grid__item-slide--'+[i]);
        $(this).append(
          "< img src='"+TM_ugcImages_box2[i]['image_url']+"'/ >"+
          "< span class='mytommymag-grid__cta' >"+TM_ugcImages_box2[i]['full_name']+"
          @"+TM_ugcImages_box2[i]['username']+"< /span >"
        );
      });
    }
    // MyTommyMag Box 3
    for(i = result.content.elements.length-TM_latestImages; i <  result.content.elements.length; i++){
      TM_ugcImages_box3[TM_counter3] = {};
      var element = result.content.elements[i];
      TM_ugcImages_box3[TM_counter3]['image_url'] = element.data.image.src;
      TM_ugcImages_box3[TM_counter3]['full_name'] = element.attribution.name; 
      if (element.source.name === 'twitter') {
        TM_ugcImages_box3[TM_counter3]['username'] = element.source.username;
        } else {
          TM_ugcImages_box3[TM_counter3]['username'] = element.meta.author_name;
      }
      TM_counter3++;
    }
    TM_ugcImages_box3 = TM_shuffle(TM_ugcImages_box3);
    if($(".mytommymag-grid").length) { 
      $('.mytommymag-grid__image-wrapper .ugc-box-3').each(function(i){
        $(this).addClass('mytommymag-grid__item-slide--'+[i]);
        $(this).append(
          "< img src='"+TM_ugcImages_box3[i]['image_url']+"'/ >"+
          "< span class='mytommymag-grid__cta' >"+TM_ugcImages_box3[i]['full_name']+"  @"+TM_ugcImages_box3[i]['username']+"< /span >"
        );
      });
    }
  }
}).fail(function() {
}).always(function() {
  TM_startAjaxCycle();
});
function TM_shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;
  // While there remain elements to shuffle...
  while (0 !== currentIndex) {
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  };
  return array;
}

Changes

  • Variables were given an additional specifies “TM_” as a safety net for they’re not colliding with variables in Tommy’s dev environment.
  • Dynamically inserting div elements for each image in the carousel.
  • AJAX request for error handling. Considering failed requests, timeouts, and what happened on the API request limit (40 calls) wasn’t fun, but having the content being added in dynamically helped. Having the content added dynamically also helped because this meant that if the client decided they wanted to show the most recent 10 images, instead of the set 20, only the TM__latestImages variable would need to be updated (no HTML markup files would need editing, as would’ve been the case if the div elements were hardcoded).

Looking back at this code, a quick edit that pops out is having encapsulated the API request and variables inside an anonymous self-calling function: (function() { //code from below })();

With this in place the variable wouldn’t have need to be changed, given that they’d be self-contained and wouldn’t share a namespace with Tommy’s internal JS code.

There are always ways to improve your code, and figuring out these small changes that can have a big impact on security, efficiency and readability comes with practice.

Check out the full project: http://usa.tommy.com/shop/en/thb2cus/mytommymag

And now, on to the next project!