Building an Online TV Schedule

I had no idea what a challenge an online TV schedule listing page was until I started working at a TV station and had to build one of these pages myself. There is a ton of wildly varying information that needs to be presented -- show titles, episode titles, upcoming episode air dates -- all of that at a minimum. Furthermore, with things being organized by time and duration, there are major layout concerns. And with layout concerns there are always accessibility concerns.

The TV schedule on the previous iteration of the station website was impressive, having been built by a much better developer than me years ago. But for all its wealth of information, it presented a number of security and stability issues as the site grew older.

As I designed the new station website, I turned to a PBS-provided TV Schedule API that would drive the construction of the new TV schedule pages. This API has its shortcomings, for one thing it doesn't provide a lot of the details the previous TV schedule system made available, but it's far more secure, accurate, and easy to update on the fly.

Here's a sample of the data that it makes available:

{
  "scheduleDate": "20180612",
  "timestampRounded": "11:00 am",
  "timestamp": "1121",
  "listings": [
    {
      "listings": [
        {
          "closed_captions": true,
          "special_warnings": "",
          "description": "Katty Kay in Washington and Christian Fraser in London return to report on the events that are shaping the world.",
          "airing_type": "",
          "start_time": "0000",
          "show_id": "episode_101551",
          "program_id": 10321,
          "episode_description": "Katty Kay in Washington and Christian Fraser in London report on the events shaping the world.",
          "package_id": null,
          "season_premiere_finale": "None",
          "duration": 30,
          "hd": true,
          "stereo": true,
          "title": "Beyond 100 Days",
          "type": "episode",
          "nola_episode": "192",
          "episode_title": "06-12-2018",
          "animated": false,
          "minutes": 30,
          "nola_root": "BEOD"
        },
        {
          "closed_captions": true,
          "special_warnings": "",
          "description": "The latest news, business, and weather every hour with stories and analysis from Japan, the rest of Asia, and around the world.",
          "airing_type": "",
          "start_time": "0030",
          "program_id": 9288,
          "package_id": null,
          "season_premiere_finale": "None",
          "duration": 30,
          "hd": false,
          "stereo": true,
          "title": "NHK Newsline",
          "type": "program",
          "nola_episode": "",
          "animated": false,
          "minutes": 30,
          "nola_root": ""
        },
      ],
      "short_name": "KLRUDT",
      "digital_channel": "18.1",
      "analog_channel": "",
      "full_name": "KLRU HDTV",
      "timezone": "US/Central"
    }
  ]
};

Making a Time Table

Armed with a JSON object filled with arrays of listings for each day for each of KLRU's four channels, I needed to present this information in a coherent and navigable way. I tried out a ton of layouts and implementations to make this work, like various tabbed views (that focused on one channel at a time), scroll events, click events, etc. The solution I eventually landed on was to send that JSON object to the DOM as a variable named schedulePage. When Express rendered the page, I laid the listings out in a table. I used Handlebars to template out the page, producing something like the following:

<table class="listings">
  <tbody>
    {{#each schedulePage.listings.[0].listings}}
      <tr class="listing">
        {{formatSchedulePageTime start_time duration}}
        <td class="schedule-page-listing">
          <p><strong>{{title}}</strong> - <a href="{{program_id }}/{{episode_id}}">{{episode_title}}</a></p>
        </td>
      </tr>
    {{/each}}
  </tbody>
</table>

Showing What's Now Playing

Each row has a class of listing. The formatSchedulePagetime helper function takes in a start time and duration, uses the duration to calculate an end time, then determines whether the current time is between the start and end time of a show. If so, that is the show that is currently playing, and its <td> element is given a class of currentTime, producing HTML that looks like the following:

<tr class="listing">
  <td class="schedule-page-time-label currentTime"><p>11:30 am</p><p class="now-playing-tag">Now Playing</p></td>
  <td class="schedule-page-listing">
    <p><strong>Peg + Cat</strong> - <a href="/episode/episode_94445">The Hotel Problem; Another Hotel Problem</a></p>
  </td>
</tr>

I can now use some JavaScript to select the element that has the class of "currentTime", and append a styled paragraph tag that says "Now Playing" in red font.

const nowPlaying = document.querySelector('.currentTime');
const node = document.createElement('p');
node.classList = 'now-playing-tag';
const textnode = document.createTextNode('Now Playing');
node.appendChild(textnode);
nowPlaying.appendChild(node);

Hiding and Showing What's Already Played

So now we have marked what's currently playing at the time that a user visits the page. The next step is to hide shows that had already played earlier in the day, toggling their visibility with the click of a "See Previous" button. Here's the code for that button, which is appended to the DOM before the table.

<div class="see-previous">
  <button>SEE PREVIOUS</button>
</div>
<table class="listings">

It's important to use a button element here, so that the "See Previous" element can receive focus for keyboard users.

Now I get to use some ES6 methods. I need to to create an array out of listing elements in the listings table, find the index of the item that had the class of currentTime, then apply a class of previous to all array items before the currentTime item. I can hide elements with the previous class name using CSS display: none;.

The next step is to add a clickEvent listener onto the See Previous button, so that when that button is clicked, the CSS property of display: none is toggled for everything with a previous class, as well as the See Previous button. In essence, the button disappears, and previous listings appear.

const listings = Array.from(document.querySelectorAll('.listing'));
const nowIndex = listings.findIndex(listing => listing.children[0].classList.contains('currentTime'));
const previous = listings
  .filter((listing, i) => i < nowIndex)
  .map(listing => listing.classList.add('previous'));
const previousListings = document.querySelectorAll('previous');
const seePreviousButton = document.querySelector('.see-previous');
seePreviousButton.addEventListener('click', function(e) {
  e.preventDefault();
  [].map.call(document.querySelectorAll('.previous'), function(el) {
    // classList is the key here - contains functions to manipulate
    // classes on an element
    el.classList.toggle('previous');
  });
  this.classList.add('no-display');
});

With all of the above, the user is presented with a time table, showing a day's listings on one of the TV station channels. The show that's currently playing is clearly marked with "Now Playing". The default view has the currently playing show as the first item of the table, but there is a "See Previous" button that, when interacted with, will display shows that had played earlier in the day, which had previously been obscured using CSS.