X marks the spot

By Lachlan Hardy
1620h Friday, 10 September 2010 Permalink

In early May now, Rick Olson re-launched Calendar About Nothing, the Seinfeld calendar for your open source commits on GitHub.

Its revival reminded me of an associated project of mine, the Seinfeld Badge. A quick and dirty JavaScript widget to show off your current streak of public commits on your blog, I've always been dissatisfied with the code. So dissatisfied that I didn't blog about it the first time I wrote it. Time to make it right!

Or at least modernise it somewhat (and then forget about this half-written blog post for 4 months while changing jobs and the like).

I like open software

Calendar About Nothing tracks my public contributions on GitHub .

HTML

The original HTML is pretty straightforward and didn't need changing. The calendar simply gets appended to this content anyway.

<div class="seinfeld-badge user-lachlanhardy">
    <h2>I like open software</h2>
    <p><a href="http://calendaraboutnothing.com/~lachlanhardy" rel="me">Calendar About Nothing</a> tracks my public contributions on <a href="http://github.com/lachlanhardy/">GitHub</a>.</p>
</div> <!-- .seinfeld-badge .user-lachlanhardy -->
Download file: index.html

CSS

/****
Seinfeld Badge
http://github.com/lachlanhardy/seinfeld-badge
20100516
****/

.seinfeld-badge {margin: 0 0 2em 0;}
    .seinfeld-badge #calendar {border-bottom: 1px solid #000; border-top: 2px solid #000; margin: 1em 0 0 0; padding: 0 5px 5px 5px;}
        .seinfeld-badge table {width: 100%;}
            .seinfeld-badge th.monthName {color: #b20000; font-weight: 700; letter-spacing: 5px; padding: 5px 0 0 0; text-align: center; text-transform: uppercase;}
            .seinfeld-badge tr.dayName th {font-weight: 700; letter-spacing: 2px; padding: 5px 0;  text-align: center; text-transform: uppercase;}
            .seinfeld-badge td {position: relative; text-align: center;}
            .seinfeld-badge td.otherMonth {color: #ccc;}
            .seinfeld-badge td.today {color: green;}
                .seinfeld-badge .xmarksthespot {margin: 1% 0 0 0; position: absolute; z-index: 100;}
                .seinfeld-badge .otherMonth .xmarksthespot {opacity: 0.2;}
            .seinfeld-badge p {margin: 0;}

    .seinfeld-badge div.streaks {font-size: 0.8em; margin: 0.2em 0.4em 0 0.4em;}
        .seinfeld-badge div.streaks span.type {font-weight: 700; text-transform: capitalize;}
        .seinfeld-badge div.streaks strong a {font-style: normal; font-weight: 300;}
    .seinfeld-badge div.longest_streak {float: right; margin: -1.3em 0 0 0; width: auto;}
    .seinfeld-badge p.pimpage {color: #ccc; font-size: 0.8em; margin: 0.5em 0.4em 0 0.4em; text-align: right;}
Download file: seinfeld-badge.css

The only change here from the original is because I've removed the reset.css. When I originally wrote the widget, I had no idea that Rick would actually incorporate it into the site. But he did and so in this re-write I wanted to strip back complexity for widget users. The reset.css was just too much load and it was confusing some people.

The result is that I had to bullet-proof my CSS a bit and in the end, that meant using divs for the content below the calendar, rather than ps.

JavaScript

The JavaScript was the focus of the refactor. I took it from 31 confusing lines with two Ajax requests to 26 easily readable lines with only one Ajax request.

The old code

(function seinfeldBadge() {
   $(".seinfeld-badge").each(function() {
        var $seinfeld = $(this),
            username = $seinfeld.attr("class").replace(/.*user-([a-z0-9]+).*/gi, "$1");
       
        $.getJSON("http://pipes.yahoo.com/pipes/pipe.run?_id=6ff23978d8e18aa8f62b196cd7d0fe78&_render=json&username=" + username + "&_callback=?", function(calendar){
            var $table = $('<div id="calendar"/>').html(calendar.value.items[0].content);
            $table.find("thead th:contains('Month')").remove();
            $("th.monthName", $table).attr("colspan", "8");
            $seinfeld.append($table);

            $("td.progressed", $table).each(function(){
                var $progressed = $(this),
                    $x = $('<div class="xmarksthespot"/>').css("height", $progressed.height())
                                                        .css("width", $progressed.width())
                                                        .append('<img src="http://calendaraboutnothing.com/images/x_1.png" height="80%" width="50%">');
             $progressed.append($x);
            });
        });

        $.getJSON("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url%3D%22http%3A%2F%2Fwww.calendaraboutnothing.com%2F~" + username + "%22%20and%20xpath%3D'%2F%2Fdiv%5B%40id%3D%22stats%22%5D%2Fdiv'%0A%20%20%20%20&format=json&callback=?", function(data){
            var $currentStreak = $("<strong>Current Streak: </strong>").append("<em>"+ data.query.results.div[0].span[1].content + "</em>"),
                $longestAnchor = $("<a/>").attr("href", data.query.results.div[1].a.href).text(data.query.results.div[1].a.span[1].content),
                $longestStreak = $("<strong class=\"longest\">Longest Streak: </strong>").append($longestAnchor),
                $streaks = $("<p class=\"streaks\"/>").append($currentStreak)
                                                      .append($longestStreak);
            $seinfeld.append($streaks);
            $seinfeld.append($('<p class="pimpage"><a href="http://github.com/lachlanhardy/seinfeld-badge">Want your own badge?</a></p>'));
        });
    });
})();
Download file: old-seinfeld-badge.js

It's messy, disorganised and doesn't follow any discernible pattern. Variables are defined wherever they're needed. Two HTTP requests means constructing the metadata separately from the calendar itself. There's lots of HTML generated and appended along the way.

The new code

(function seinfeldBadge() {
   $(".seinfeld-badge").each(function() {
        var $seinfeld = $(this),
            username = $seinfeld.attr("class").replace(/.*user-([a-z0-9]+).*/gi, "$1"),
            $table,
            $streaks,
            $progressed,
            $x;
        $.getJSON("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url%3D%22http%3A%2F%2Fcalendaraboutnothing.com%2F~" + username + "%22%20and%20xpath%3D%27%2F%2Fdiv%27&format=xml&callback=?", function (data){
            $table = $(data.results[2]);
            $streaks = $("<div class=\"streaks\"/>").append(data.results[5]).append(data.results[6]);
            $table.find("thead th:contains('Month')").remove()
            $table.find("th.monthName").attr("colspan", "8");
            $seinfeld.append($table);
            $table.find("td.progressed").each(function(){
                $progressed = $(this);
                $x = $('<div class="xmarksthespot"/>').css({
                    "height": $progressed.height(),
                    "width": $progressed.width()
                }).append('<img src="http://calendaraboutnothing.com/images/x_1.png" height="80%" width="50%">');
                $progressed.append($x);
             });
            $seinfeld.append($streaks).append('<p class="pimpage"><a href="http://github.com/lachlanhardy/seinfeld-badge">Want your own badge?</a></p>');
        });
    });
})();
Download file: seinfeld-badge.js

I've ordered the variables, and organised the structure a bit better, but the real win here lies in a new feature from YQL. I'm a big fan of YQL but when I saw they'd created a new data format for their responses, I was pretty confused for a while. What the hell was JSONP-X?!

Once I found the docs, though, I got it immediately. All you have to do is request an XML response from YQL with a callback and it'll send you XML in a JSON wrapper. It sounds like a monstrosity but this is perfect for screenscraping. And perfect for the Seinfeld Badge.

This widget has always relied on ripping the table for the calendar directly from a Calendar About Nothing user page. Constructing all those table cells again to correctly display this month would be inelegant code and an inelegant solution.

This was the greatest failing of the old version. I couldn't retrieve the HTML from the page via YQL because it either returned full XML or split up all the data into valid JSON. Hence the two requests: one to grab the HTML calendar from Yahoo! Pipes and another to grab the metadata.

JSONP-X means that YQL can grab me everything I need in one request (a link to the YQL console — requires login). Then I just select which divs I want to display and style them myself.

Summary

I'm much happier with the new version. Although writing it up 4 months after the refactor means I'm already thinking of things I'd do differently now. I guess that's a good thing!

Thanks for Rick and Kyle for building Calendar About Nothing. It's a great motivator! Even though my current performance is shabby…

Get your own badge from the GitHub repo (I welcome improvements!) or directly from Calendar About Nothing itself.