About Us

Bulletpoint StarImulus® is a technology focused design + interactive agency.

In addition to our client services we also have a few products in the works. Our office is always filled with chatter and this blog is an outlet for our creative energy, rants and ideas.

Podium

Stacks!
Imulus built a task management solution that finally works for teams. It's a life saver, learn more at usestacks.com.

Featured Project

Author: Casey ()

May13

Developing responsive designs in Chrome

I recently switched from docking Chrome’s dev tools on the bottom of the browser to the right side. I like it better because it gives me more vertical space for inspecting the DOM and debugging JS.

One nice side effect of docking on the right is that it makes developing responsive designs much easier. Instead of resizing the entire Chrome window, you can instead pull the divider between the viewport and the dev tools.

chrome-responsive

Apr26

I would have written you a shorter letter, but I did not have the time.

After we released retina.js, we were surprised and proud to see a huge influx of traffic. It was being shared all over the place. One pattern I noticed was most sites that were writing about it had copy-and-pasted the intro text straight from the site.

retina.js is an open source script that makes it easy to serve

high-resolution images to devices with retina displays

This is a good thing. We can describe it better than anyone else, after all. But seeing the same block of copy all over the web made me wonder if I could have written it better. Could I have articulated its purpose more clearly? Could I have been more concise? Could it have been shorter, less words, less syllables?

I wrote the copy a bit hastily in a rush to get the site up. I certainly didn’t expect such a positive response, so I wonder if I would have written it differently if I had.

full page viewposted in: meta

Apr25

Retina.js

Days before the iPad 3 shipped, Bruce, Taylor and I were talking about how incredible the new retina display was going to be. In a moment of terror it occurred to all of us how it — one device — was going to forever change web design as we know it.

Everything we built would now be half the resolution it should be. Every photo, every icon, every logo; doomed to a sad state of blurriness on retina displays. We needed a way to serve high-res images to high-res displays. So we built it.

Retina.js is a zero-config script that makes it easy to serve high-resolution versions of your website’s images to devices with retina displays. It checks all of the images on your page to see if there is a high-res version on your server. If it finds one then it swaps it in. You don’t need to change any of your markup, you simply need to include one script on your page and it just works.

Go check it out.

It’s open source, of course, so we’d love your help in making it better. Fork us on GitHub.

Mar9

What I love about Hype: From a designer and a developer’s perspective

From the Designer’s perspective (Kat):

Since Apple’s excommunication of Adobe Flash on their iOS platform, the web has seen an interesting trend: Flash is out, Javascript is in. Okay, that may be a bit of an exaggeration. Flash has seen a downward trend even before Apple’s cast-off. With its limitations in Google search, load time, selectable text, updatability… must I go on?

So what’s a poor designer/animator to do? Continue to insist on our beloved Flash that we love and hate all at the same time? Learn Javascript? Sit in the corner and cry? Well, I’ve done 2 out of the 3, and it’s just not pretty. Fortunately, I was introduced to a better solution: Tumult Hype app for Mac OS X.

Here is what is so flippin’ great about Hype:

1. Jump right in, the water is fine

After watching less that 2 minutes of Hype tutorials, I jumped in guns blazing and I never looked back. I have prior experience with Adobe Flash and Adobe After Effects and Hype uses keyframe-based animation so I had a bit of a stepping stone of knowledge before I started. However, I was rarely stumped.

2. Cheap!

$29.99! However, this is a limited time pricing for version 1.0.

3. Record button

This thing is a bit of a love and hate feature. You turn on the record button and Hype will automatically keyframe size, opacity, location, etc. This is great, but if you forget it’s turned on, it can be annoying. But that’s more user error.

What I don’t like about Hype:

Keep in mind, this is still a new product, so of course they are working to get the kinks out, but the layer functionality is lacking. There is no way to lock layers so trying to animate a buried layer can be difficult. What’s more is ordering layers is buggy. Fortunately another pro about hype is the customer service. I’ve reported both of these cons and they were quick to respond. We’ll see if they’re also quick to fix.

From the Developer’s perspective (Casey):

1. It looks great on mobile devices

Unlike Flash, Hype pieces actually render on mobile devices — and they look fantastic. More than once we’ve seen a Hype piece look better on an iPhone than on a Mac. No joke.

2. Drop-in replacement

As Flash becomes more and more passé, we’re often asked to replace Flash pieces with a non-Flash equivalent. From a development perspective, Hype is great for this. Most of the time we can drop in Hype to replace Flash with no changes to the stylesheet or surrounding markup.

3. Open code

Though the JavaScriot code generated by Hype is compressed, if we need to open it up and make changes me can. This grants us the opportunity to hook up Hype to other services right on the front end. This is something that was much harder to accomplish in the Flash days.

Mar3

Introducing NomNom

Lunch is an important daily ritual at Imulus. Almost every day a group steps out for a bite at one of Boulder’s many dining establishments — and sometimes the entire office will go out together. We’ve learned that it’s nearly impossible to get a group of 12 people to agree on where to eat.

One day, in a fit of hungry frustration, someone mentioned, “I wish there was an app that just told us where to go to lunch.”

So we built it.

Enter NomNom: an app that makes it easy for groups to decide where to eat. NomNom learns where your group likes to go and helps you find new restaurants or rediscover old favorites.

NomNom uses Yelp to find restaurants and bars in your area and gives you recommendations on where you should go. People in your group vote on where they want to go or recommend a place of their own to the rest of the group.

nomnom-voting-screen

We’ve been using NomNom every day for about four months now and it’s been great. It’s exactly what we needed and we’re very proud to invite everyone else to start using it.

Go check it out:
http://nomnomapp.com

And then let us know how you like it:
hello@nomnomapp.com or @nomnomapp

Dec9

Reserved usernames to avoid vanity URL collision

As we continue to add support for more languages on our tech support management application, Support Details, one thing we’ve considered is how vanity URLs will impact the introduction of new features. In the short future we plan to add support for custom accounts, so it made sense to compile a list of usernames we’d reserve out of the gate to avoid collision in the future.

Here’s our running list:


reserved_usernames = [
	# Companies we'd love to have use our service
	# so we'll reserve them to be safe
	'supportdetails',
	'support-details',
	'stacks',
	'imulus',
	'github',
	'twitter',
	'facebook',
	'google',
	'apple',

	# Generic reserved words
	'about',
	'account',
	'activate',
	'add',
	'admin',
	'administrator',
	'api',
	'app',
	'apps',
	'archive',
	'archives',
	'auth',
	'blog',
	'cache',
	'cancel',
	'careers',
	'cart',
	'changelog',
	'checkout',
	'codereview',
	'compare',
	'config',
	'configuration',
	'connect',
	'contact',
	'create',
	'delete',
	'direct_messages',
	'documentation',
	'download',
	'downloads',
	'edit',
	'email',
	'employment',
	'enterprise',
	'faq',
	'favorites',
	'feed',
	'feedback',
	'feeds',
	'fleet',
	'fleets',
	'follow',
	'followers',
	'following',
	'friend',
	'friends',
	'gist',
	'group',
	'groups',
	'help',
	'home',
	'hosting',
	'hostmaster',
	'idea',
	'ideas',
	'index',
	'info',
	'invitations',
	'invite',
	'is',
	'it',
	'job',
	'jobs',
	'json',
	'language',
	'languages',
	'lists',
	'login',
	'logout',
	'logs',
	'mail',
	'map',
	'maps',
	'mine',
	'mis',
	'news',
	'oauth',
	'oauth_clients',
	'offers',
	'openid',
	'order',
	'orders',
	'organizations',
	'plans',
	'popular',
	'post',
	'postmaster',
	'privacy',
	'projects',
	'put',
	'recruitment',
	'register',
	'remove',
	'replies',
	'root',
	'rss',
	'sales',
	'save',
	'search',
	'security',
	'sessions',
	'settings',
	'shop',
	'signup',
	'sitemap',
	'ssl',
	'ssladmin',
	'ssladministrator',
	'sslwebmaster',
	'status',
	'stories',
	'styleguide',
	'subscribe',
	'subscriptions',
	'support',
	'sysadmin',
	'sysadministrator',
	'terms',
	'tour',
	'translations',
	'trends',
	'unfollow',
	'unsubscribe',
	'update',
	'url',
	'user',
	'weather',
	'webmaster',
	'widget',
	'widgets',
	'wiki',
	'ww',
	'www',
	'wwww',
	'xfn',
	'xml',
	'xmpp',
	'yaml',
	'yml',

	# Top 50 languages by speaking population
	'chinese',
	'mandarin',
	'spanish',
	'english',
	'bengali',
	'hindi',
	'portuguese',
	'russian',
	'japanese',
	'german',
	'wu',
	'javanese',
	'korean',
	'french',
	'vietnamese',
	'telugu',
	'chinese',
	'marathi',
	'tamil',
	'turkish',
	'urdu',
	'min-nan',
	'jinyu',
	'gujarati',
	'polish',
	'arabic',
	'ukrainian',
	'italian',
	'xiang',
	'malayalam',
	'hakka',
	'kannada',
	'oriya',
	'panjabi',
	'sunda',
	'panjabi',
	'romanian',
	'bhojpuri',
	'azerbaijani',
	'farsi',
	'maithili',
	'hausa',
	'arabic',
	'burmese',
	'serbo-croatian',
	'gan',
	'awadhi',
	'thai',
	'dutch',
	'yoruba',
	'sindhi',

	# Country TLDs
	'ac',  # Ascension Island
	'ad',  # Andorra
	'ae',  # United Arab Emirates
	'af',  # Afghanistan
	'ag',  # Antigua and Barbuda
	'ai',  # Anguilla
	'al',  # Albania
	'am',  # Armenia
	'an',  # Netherlands Antilles
	'ao',  # Angola
	'aq',  # Antarctica
	'ar',  # Argentina
	'as',  # American Samoa
	'at',  # Austria
	'au',  # Australia
	'aw',  # Aruba
	'ax',  # and
	'az',  # Azerbaijan
	'ba',  # Bosnia and Herzegovina
	'bb',  # Barbados
	'bd',  # Bangladesh
	'be',  # Belgium
	'bf',  # Burkina Faso
	'bg',  # Bulgaria
	'bh',  # Bahrain
	'bi',  # Burundi
	'bj',  # Benin
	'bm',  # Bermuda
	'bn',  # Brunei Darussalam
	'bo',  # Bolivia
	'br',  # Brazil
	'bs',  # Bahamas
	'bt',  # Bhutan
	'bv',  # Bouvet Island
	'bw',  # Botswana
	'by',  # Belarus
	'bz',  # Belize
	'ca',  # Canada
	'cc',  # Cocos (Keeling) Islands
	'cd',  # Democratic Republic of the Congo
	'cf',  # Central African Republic
	'cg',  # Republic of the Congo
	'ch',  # Switzerland
	'ci',  # Côte d'Ivoire
	'ck',  # Cook Islands
	'cl',  # Chile
	'cm',  # Cameroon
	'cn',  # People's Republic of China
	'co',  # Colombia
	'cr',  # Costa Rica
	'cs',  # Czechoslovakia
	'cu',  # Cuba
	'cv',  # Cape Verde
	'cx',  # Christmas Island
	'cy',  # Cyprus
	'cz',  # Czech Republic
	'dd',  # East Germany
	'de',  # Germany
	'dj',  # Djibouti
	'dk',  # Denmark
	'dm',  # Dominica
	'do',  # Dominican Republic
	'dz',  # Algeria
	'ec',  # Ecuador
	'ee',  # Estonia
	'eg',  # Egypt
	'eh',  # Western Sahara
	'er',  # Eritrea
	'es',  # Spain
	'et',  # Ethiopia
	'eu',  # European Union
	'fi',  # Finland
	'fj',  # Fiji
	'fk',  # Falkland Islands
	'fm',  # Federated States of Micronesia
	'fo',  # Faroe Islands
	'fr',  # France
	'ga',  # Gabon
	'gb',  # United Kingdom
	'gd',  # Grenada
	'ge',  # Georgia
	'gf',  # French Guiana
	'gg',  # Guernsey
	'gh',  # Ghana
	'gi',  # Gibraltar
	'gl',  # Greenland
	'gm',  # The Gambia
	'gn',  # Guinea
	'gp',  # Guadeloupe
	'gq',  # Equatorial Guinea
	'gr',  # Greece
	'gs',  # South Georgia and the South Sandwich Islands
	'gt',  # Guatemala
	'gu',  # Guam
	'gw',  # Guinea-Bissau
	'gy',  # Guyana
	'hk',  # Hong Kong
	'hm',  # Heard Island and McDonald Islands
	'hn',  # Honduras
	'hr',  # Croatia
	'ht',  # Haiti
	'hu',  # Hungary
	'id',  # Indonesia
	'ie',  # Republic of Ireland  Northern Ireland
	'il',  # Israel
	'im',  # Isle of Man
	'in',  # India
	'io',  # British Indian Ocean Territory
	'iq',  # Iraq
	'ir',  # Iran
	'is',  # Iceland
	'it',  # Italy
	'je',  # Jersey
	'jm',  # Jamaica
	'jo',  # Jordan
	'jp',  # Japan
	'ke',  # Kenya
	'kg',  # Kyrgyzstan
	'kh',  # Cambodia
	'ki',  # Kiribati
	'km',  # Comoros
	'kn',  # Saint Kitts and Nevis
	'kp',  # Democratic People's Republic of Korea
	'kr',  # Republic of Korea
	'kw',  # Kuwait
	'ky',  # Cayman Islands
	'kz',  # Kazakhstan
	'la',  # Laos
	'lb',  # Lebanon
	'lc',  # Saint Lucia
	'li',  # Liechtenstein
	'lk',  # Sri Lanka
	'lr',  # Liberia
	'ls',  # Lesotho
	'lt',  # Lithuania
	'lu',  # Luxembourg
	'lv',  # Latvia
	'ly',  # Libya
	'ma',  # Morocco
	'mc',  # Monaco
	'md',  # Moldova
	'me',  # Montenegro
	'mg',  # Madagascar
	'mh',  # Marshall Islands
	'mk',  # Republic of Macedonia
	'ml',  # Mali
	'mm',  # Myanmar
	'mn',  # Mongolia
	'mo',  # Macau
	'mp',  # Northern Mariana Islands
	'mq',  # Martinique
	'mr',  # Mauritania
	'ms',  # Montserrat
	'mt',  # Malta
	'mu',  # Mauritius
	'mv',  # Maldives
	'mw',  # Malawi
	'mx',  # Mexico
	'my',  # Malaysia
	'mz',  # Mozambique
	'na',  # Namibia
	'nc',  # New Caledonia
	'ne',  # Niger
	'nf',  # Norfolk Island
	'ng',  # Nigeria
	'ni',  # Nicaragua
	'nl',  # Netherlands
	'no',  # Norway
	'np',  # Nepal
	'nr',  # Nauru
	'nu',  # Niue
	'nz',  # New Zealand
	'om',  # Oman
	'pa',  # Panama
	'pe',  # Peru
	'pf',  # French Polynesia
	'pg',  # Papua New Guinea
	'ph',  # Philippines
	'pk',  # Pakistan
	'pl',  # Poland
	'pm',  # Saint-Pierre and Miquelon
	'pn',  # Pitcairn Islands
	'pr',  # Puerto Rico
	'ps',  # Palestinian territories
	'pt',  # Portugal
	'pw',  # Palau
	'py',  # Paraguay
	'qa',  # Qatar
	're',  # Réunion
	'ro',  # Romania
	'rs',  # Serbia
	'ru',  # Russia
	'rw',  # Rwanda
	'sa',  # Saudi Arabia
	'sb',  # Solomon Islands
	'sc',  # Seychelles
	'sd',  # Sudan
	'se',  # Sweden
	'sg',  # Singapore
	'sh',  # Saint Helena
	'si',  # Slovenia
	'sj',  # Svalbard and  Jan Mayen Islands
	'sk',  # Slovakia
	'sl',  # Sierra Leone
	'sm',  # San Marino
	'sn',  # Senegal
	'so',  # Somalia
	'sr',  # Suriname
	'ss',  # South Sudan
	'st',  # São Tomé and Príncipe
	'su',  # Soviet Union
	'sv',  # El Salvador
	'sy',  # Syria
	'sz',  # Swaziland
	'tc',  # Turks and Caicos Islands
	'td',  # Chad
	'tf',  # French Southern and Antarctic Lands
	'tg',  # Togo
	'th',  # Thailand
	'tj',  # Tajikistan
	'tk',  # Tokelau
	'tl',  # East Timor
	'tm',  # Turkmenistan
	'tn',  # Tunisia
	'to',  # Tonga
	'tp',  # East Timor
	'tr',  # Turkey
	'tt',  # Trinidad and Tobago
	'tv',  # Tuvalu
	'tw',  # Republic of China (Taiwan)
	'tz',  # Tanzania
	'ua',  # Ukraine
	'ug',  # Uganda
	'uk',  # United Kingdom
	'us',  # United States of America
	'uy',  # Uruguay
	'uz',  # Uzbekistan
	'va',  # Vatican City
	'vc',  # Saint Vincent and the Grenadines
	've',  # Venezuela
	'vg',  # British Virgin Islands
	'vi',  # United States Virgin Islands
	'vn',  # Vietnam
	'vu',  # Vanuatu
	'wf',  # Wallis and Futuna
	'ws',  # Samoa
	'ye',  # Yemen
	'yt',  # Mayotte
	'yu',  # Yugoslavia
	'za',  # South Africa
	'zm',  # Zambia
	'zw'   # Zimbabwe
]

It is a merged list of the recommendations from this Quora discussion, this list of country TLDs and
this list of languages

How to Contribute:
Gist: https://gist.github.com/1453705

Dec1

Powder: A Backbone-powered iPad webapp

Powder screenshot

Winter is upon us, and like many folks in Colorado we’re stoked to hit the mountains. Given the recent snowfall and our experimentation with HTML5 localStorage, it seemed natural to push it a bit further and do something fun.

Enter Powder, an HTML5 webapp for tracking the snow conditions at your favorite ski resorts.

Powder is an experiment using backbone.js to power an iPad webapp. On the client, Powder uses the browser’s native HTML5 localStorage mechanism to save the users’s preferences, the list of resorts the user chooses to track. On the server, Powder uses Sinatra to fetch and parse various snow/ski reports from http://onthesnow.com/ then serve it up as JSON to the browser.

The Stack

Powder is free and open source, hosted on GitHub. We’d love to have you join us in making Powder better. Feel free to fork the project and help us make a kickass iPad webapp.

Nov28

An Interesting Challenge

Last week we had a client call us with an interesting problem. They have a tradeshow kiosk they’d like to use to capture form data from users and collect it at the end of each day.

The challenge: the kiosk doesn’t have an internet connection and can’t run a webserver. It can display webpages, but only through an IE8 iframe.

If the kiosk had a net connection we’d build a static form to post the data to a remote webservice. But that’s out. If the kiosk could run a webserver we’d build a simple webapp to handle the data locally. But that’s out, too.

We decided to solve the problem by saving the data to localStorage. localStorage is a key-value store built into most modern browsers, and a perfect mechanism for persisting simple form data. It has a minimal JavaScript API (setters and getters) via the window’s localStorage object that makes it really easy to work with.

Here’s how we did it:

We started by setting up a simple Storage class to interface with localStorage. This isn’t entirely necessary, but makes it easier to save and retrieve the data without having to parse/stringify on every transaction. (Yes, we love CoffeeScript.)


class Storage

  constructor: (@name)->
    @data = []

    # Check to see if the object already
    # exists in localStorage
    # If it does, grab the value
    # Otherwise, create a key in localStorage
    # with an empty array of data
    if localStorage[@name]?
      do @fetch
    else
      # We need to stringify the data as JSON
      # because it is stored as a string
      localStorage.setItem(@name, JSON.stringify(@data))

  # Pull the data out of localStorage and populate
  # the instance's @data array property
  # We need to parse the data as JSON
  # because it is stored as a string
  fetch: ->
    @data = JSON.parse(localStorage.getItem(@name))

  # Add an item to the instance's @data array and
  # re-save to localStorage
  add: (data) ->
    @data.push data
    localStorage.setItem(@name, JSON.stringify(@data))

Next, we set up a Form class to observe the form on the page and do some simple validation before finally committing it to a storage instance.


class Form

  constructor: (@$form, name)->
    @storage  = new Storage(name)
    @$fields  = $('input, textarea, select', @$form)

    @$form.submit (event) =>
      event.preventDefault()
      if @validate()
        do @save
        do @clear

  save: ->
    data = {}

    @$fields.each ->
      name  = $(this).attr('name')
      value = $(this).val()
      data[name] = value

    @storage.add data

  clear: ->
    @$fields.each ->
      $field = $(this)
      type = $field.attr('type')
      $field.val('') if type is 'text'

  validate: ->
    data = {}
    valid = true

    # Simple 'required' validation
    # Loop over each of the form fields
    # If the field is required, then check to see if it is blank
    @$fields.each ->
      $field = $(this)
      value = $field.val()
      required = $field.hasClass('required')
      if required and value is ''
        valid = false
        $field.addClass('invalid')
      else
        $field.removeClass('invalid')

    return valid

The client needed a means of retrieving the data, so we set up way to grab all of the data and render out a table with each of the entries.


class ResultsTable
  constructor: (@$table, name) ->
    @storage  = new Storage(name)
    @results = @storage.data
    $head_tr = $('<tr />')

    for field, value of @results[0]
      $head_tr.append $('<td />').text(field)

    $('thead', @$table).append $head_tr

    for result in @results
      $tr = $('<tr />')
      $tr.append $('<td />').text(value) for field, value of result
      $('tbody', @$table).append $tr

Finally, an example of how to pull it all together.

On the form page:


<form id="user-form" action="#">
  <input type="text" name="first_name" />
  <input type="text" name="last_name" />
  <button type="submit">Submit</button>
</form>

<script type="text/javascript">
  $(document).ready(function(){
    new Form($('form#user-form'), 'kiosk');
  });
</script>

On the results page:


<table id="results">
</table>

<script type="text/javascript">
  $(document).ready(function(){
    new ResultsTable($('#results'), 'kiosk');
  });
</script>

Given the constraints, we feel like this is great solution. Lucky for us, the browser environment was predictable and IE8 has support for localStorage. All in all, it was a fun experiment with localStorage.

Nov16

At Least Three States

A few weeks ago Microsoft released a video showcasing their idea of what the future of technology might look like. Some weeks later, Bret Victor wrote a rant about how unimaginative Microsoft’s vision is. The thrust of his argument is that the ubiquitous touchscreens depicted in the video are uninventive because it disregards the most fundamental and powerful tool known to man: our hands. He explains that the physical interfaces of today don’t take advantage of our hands’ ability to both feel and manipulate things.

All this talk about hands and feeling and manipulation got me thinking. Given the current state and inertia of technology, it should be our foremost goal to make our interfaces feel good.

Feeling is often the indescribable property that separates a good interface from a great one. It’s the gap between the pixels and your finger (or mouse, by extension). You hear people mention this all the time when talking about software; “I don’t know, it just feels good”. Of course they’re not actually talking about how it feels, they’re talking about how their brain perceives it would feel if it were a physical interface. They’re talking about how well it responds to their input.

Without tactile feedback, as is the case when using a touchscreen or navigating a mouse cursor around a screen, our brain can’t depend on our hands and instead relies on our eyes to proxy visual feedback. We use this information on a subconsious level to confirm our last action and plan our next one. How an interface feels is the fidelity of the visual feedback to how our brain predicts it should feel.

It’s easy to see how important visual feedback becomes to the user experience in the absense of tactile feedback. It explains why skeuomorphism has prevailed as a common user interface design approach: it’s comforting to us as users when what we see and feel in the virtual world matches our perception of the physical world. This is why buttons often look like buttons and calendars often looks like calendars.

But how an interface feels goes deeper than the pixels on the screen; it’s all about trust. As an interface designer, you enter a contract with your users. Your design implicitly guarantees the user: “When you click X, the software will do Y”. It’s the job of your interface to back this guarantee by providing the feedback necessary for the user to feel assured you kept your end of the bargain. The gap between what your UI promised and what was delivered is received by the user as negative feeling. We’ve all experienced it, that brief moment of cognitive dissonance when you beckon the software to do one thing and it appears to do something different, or worse, nothing. That’s broken trust.

I have long preached that every clickable element (links, buttons, etc) deserves at least three states: idle, hover and click. I also encourage additional states for active-hover and active-click, but they are less necessary. Even novice designers know to make the idle state of their links obvious, but it’s surprising how many neglect to provide the user proper hover and click states. The hover state signifies that the element is clickable at this moment and the click state confirms that the element was clicked, both of which are crucial to keeping up your end of the UI contract.

The very best user interfaces are those that have a two-way conversation with the user, with equal transmission amplitude from both parties. When the visual feedback provided by your interface falls short of what the user expected, you’ve subconsciously broken their trust and left them with an indescribable feeling of doubt.

Sep21

ClickTime API Wrappers for Ruby and JavaScript

For a few internal projects at the Imulus office, we’ve been collecting timesheet data from the tracking software we use, ClickTime. Lucky for us, they offer an API with full access to all of our data. Unfortunately, however, it’s a SOAP API.

SOAP can be a bear, so we’ve rolled some code to help alleviate the pain of working with it:
a Ruby wrapper and a JavaScript wrapper. Both are open source, and we hope they make your life easier. Below are some examples of how to use them.

Ruby

The Ruby wrapper (ruby-clicktime) comes in the form of a single file with one class, ClickTime.

Requirements

ruby-clicktime uses Savon, so be sure to install that first:

$ gem install savon

Usage

The ClickTime class has two methods:

ClickTime#actions will return a list of all actions available to the ClickTime SOAP API

clicktime.actions  # un_sync_all, get_sync_id, set_sync_id, ...

ClickTime#exec will execute an API call and return a hash of results

clicktime.exec :api_action, {params}

Example

First, create an instance of the ClickTime class:

require 'clicktime'

clicktime = ClickTime.new :key => 'api-key', :password => 'password'

The GetEmployeeList and GetClientList API methods, as defined in the ClickTime API docs:

<GetEmployeeList xmlns="http://clicktime.com/webservices/2.2/">
	<UserID>string</UserID>
	<ActiveOnly>boolean</ActiveOnly>
</GetEmployeeList>

<GetClientList xmlns="http://clicktime.com/webservices/2.2/">
	<UserID>string</UserID>
</GetClientList>

To execute these queries, you would call the following:

employees = clicktime.exec :get_employee_list,
    {"UserID" => 'your-user-id', "ActiveOnly" => 'true'}
clients = clicktime.exec :get_client_list, {"UserID" => 'your-user-id'}

Notice the action is passed as a snake_case symbol from the list of available actions. (clicktime.actions to list them)

JavaScript

The JavaScript wrapper is a CommonJS module, ready for use in your Node.js project via npm.

Installation

In your project:

$ npm install clicktime

And in your script:

var ClickTime = require('clicktime');

Example

First, create an instance of the ClickTime object:

var clicktime = new ClickTime({
  key: 'your-api-key',
  password: 'your-api-password'
});

The GetEmployeeList API method, as defined in the ClickTime API docs:

<GetEmployeeList xmlns="http://clicktime.com/webservices/2.2/">
  <UserID>string</UserID>
  <ActiveOnly>boolean</ActiveOnly>
</GetEmployeeList>

To execute this query you would call the following:

clicktime.exec('GetEmployeeList', {
    UserID: 'some-user-id',
    ActiveOnly: true
  }, function(response){ // your callback function
    console.log(response);
  }
);

Reference

Source code:
ruby-clicktime on GitHub
node-clicktime on GitHub

ClickTime SOAP API Documentation:
http://app.clicktime.com/documentation/webservices/2_2/WebServicesDocumentation_2_2.asp