Alisdair McDiarmid is a senior software engineer based in Toronto, Ontario.

Building Team Time Zone, part 3: Creating the User Interface

This is part three of a five part series on building Team Time Zone, an Ember app for keeping track of what time it is for all your Slack friends across the world.

Having built out a test version of the Slack API, and an Ember Data adapter to access it, the next step was to take the user information and present it. I had a pretty solid idea of how to go about this, but I just couldn't get started.

This often happens to me when I'm working on projects. I get stuck right at the beginning, worrying about getting the implementation badly wrong and having to throw it all away. I spend a long time trying to get the design right, and the longer I try to hold it all in my head, the less certain I get that it even makes sense.

Sometimes the only way to write good code is to write bad code, and then fix it. Make a mess, and clean it up.

Making a mess

Writing bad code on purpose can be hard to do. I find it easiest when I think of it as a first draft, or a sketch.

Like a sketch, it's important to move quickly. Jot down some FIXME comments instead of trying to implement difficult features. Write the thing that comes to mind, don't try to simplify or make it efficient.

My first cut of the UI was pretty rough. I just threw all the functionality into one giant messy component, which barely worked. Here's the template:

{{#each columns as |column|}}
  <!-- FIXME: column should be a component -->
  <div class="timezone-column {{if column.empty 'timezone-column--empty'}}">
    <!-- FIXME: users should be grouped by timezone (asc) -->
    <!-- FIXME: timezone should be displayed -->
    <!-- FIXME: current time should be displayed -->
    <!-- FIXME: item should be a component -->
    {{#each column.users as |user|}}
      <div class="timezone-column__item">
        <div><img src={{user.image_72}}></div>
        <div>{{user.realName}}</div>
        <div>{{user.tz}}</div>
        <div>{{user.tzOffset}}</div>
      </div>
    {{/each}}
  </div>
{{/each}}

Nothing about this is any good at all. For one thing, it doesn't even work properly. But it's not as bad as the component code. Look at this beautiful mess:

import Ember from 'ember';
import TimezoneColumn from 'stz/models/timezone-column';

const secondsInHour = 3600;

function calculateTimezoneStart(offset) {
  return Math.floor(offset / secondsInHour) * secondsInHour;
}

function calculateTimezoneStop(offset) {
  return Math.ceil(offset / secondsInHour) * secondsInHour;
}

function nextTimezone(start) {
  return start + secondsInHour;
}

export default Ember.Component.extend({
  classNames: ['timezone-container'],

  users: Ember.computed.alias('model'),
  offsets: Ember.computed.mapBy('users', 'tzOffset'),
  earliest: Ember.computed.min('offsets'),
  latest: Ember.computed.max('offsets'),

  columns: Ember.computed('users.@each.tzOffset', function() {
    let users = this.get('model');
    let start = calculateTimezoneStart(this.get('earliest'));
    let stop = calculateTimezoneStop(this.get('latest'));
    let columns = Ember.A();

    for (let tz = start; tz < stop; tz = nextTimezone(tz)) {
      let matches = users.filter(function(user) {
        let offset = user.get('tzOffset');
        return offset >= tz && offset < nextTimezone(tz);
      });
      columns.push(TimezoneColumn.create({
        timezoneStart: tz,
        users: matches
      }));
    }

    return columns;
  })
});

Let's start with some slightly broken functions for calculating offsets. Add a for loop with questionable termination conditions that are unclear at best. And we're creating a function inside that loop! Really confusing and inefficient use of Array.prototype.filter. No composition, no breakdown of responsibilities, barely any data model. I wrote this less than two months ago, and now I couldn't tell you exactly what it does.

But there are good things here too! Specifically, I've understood how to solve the problem, or at least how to get close. I've come up with some good names for concepts within the system.

Also, it does sort of work! Here's a screenshot to prove it:

Mega component screenshot

Clean it up

There's an important reason for making this mess. It gave me a concrete place to start building things properly. I can look at my program in a (sort-of) working state, and see the next small step to make it better, both functionally and qualitatively.

In this case, I started to break down the ridiculous mega component into smaller ones. The first design I had was a simple hierarchy of components:

Aside: I really don't like time zones

There's a problem with this, which is that time zones are awful. For some hours of the clock, there are multiple time zones. And some of those have offsets which are partial hours. I have a coworker who lives in Newfoundland, which has its own special UTC-3:30 timezone.

Component hierarchy

Because there can be multiple different time zones within one hour, that means that there can be several times in one column. So we need to group users in a column by their time zone. That means one more component, giving us the final hierarchy:

  1. Collection
  2. Column
  3. Group
  4. User

I already had most of these in the one giant component, so the direction of my work was clear: repeatedly split up the components into smaller ones, writing tests and fixing bugs as I went.

I'm fairly happy with how this went. I was able to extract testable pure functions into util files, where they could be easily unit tested (and corrected, when I found off-by-one errors). I wrote components which basically had the responsibility of splitting their input up by time zone value, which could also be unit tested. And finally there were the components that implemented the UI, which were a perfect candidate for component integration testing.

The template hierarchy in particular is quite pleasing to look at. Here's the top level template on the route:

{{timezone-collection users=filteredUsers}}

Here's the timezone-collection template:

{{#each columns as |column|}}
  {{timezone-column users=column.users}}
{{/each}}

And timezone-column:

{{#each groups as |group|}}
  {{timezone-group
    timezoneOffset=group.timezoneOffset
    users=group.users}}
{{/each}}

Then timezone-group:

<div class="timezone-group__time clickable" {{action 'toggleTimeFormat'}}>
  {{time}}
</div>

<div class="timezone-group__timezone">{{timezone}}</div>

{{#each sortedUsers as |user|}}
  {{user-profile user=user}}
{{/each}}

And finally, user-profile:

<div class="user-profile__avatar" title={{user.realName}}>
  <img src={{user.image192}} width="96" height="96" alt="">
</div>
<div class="user-profile__username" title={{user.name}}>
  {{user.name}}
</div>
<div class="user-profile__realname" title={{user.realName}}>
  {{user.realName}}
</div>

What I like so much about this is that there's barely any nesting. Only one loop per template, only a few lines each. Even the most complex template, the user profile, is easy to understand with only a minute of looking at it.

From a functionality point of view, this brought me to a pretty good spot. Not everything worked yet, but the basics were definitely there. Here's a screenshot:

Mostly working

Finishing and polishing

With a few more commits, I added the last missing features needed to make the app good enough to ship. Displaying the real local time for each group was the entire point of the app, so that came first. It was really simple: just use the awesome moment.js library to calculate the offset local time.

Once that worked, I added a timer to update the clock every second. Then almost everything else remaining was UI polish: adding navigation, fixing a few styles at mobile widths, adding this cute loading spinner:

Loading, please wait

After a few more evenings of playing with CSS gradients and animations, I eventually ran out of things to do. The UI worked, my code was well designed, I had great test coverage.

There was nothing else for it. Time to bite the bullet and try to implement authentication.


This article is part of a five-part series on building Team Time Zone:

  1. Inception, Prototype, and Planning
  2. Ember Data and the Slack API
  3. Creating the User Interface
  4. Authenticating with Slack
  5. Deployment, Release, and Future