]

Thorne Brandt

Mobile Menu

Portfolio > Development > Shiftgig

Shiftgig

Shiftgig is basically an ondemand staffing company with a pool of pre-vetted specialists in a variety of fields. This type of business model requires different interfaces for clients, workers, internal account managers, and public facing marketing for potential customers. The motion graphics intro that I animated is perhaps the best way to explain it.

A promotional/explanatory video for Shiftgig. Click to watch full video.

I was hired at Shiftgig to diagnose a loading time problem on a backbone-powered admin calendar app in 2014. Backbone was all the rage in that era, so I developed their new work stack, which consisted of grunt compiling backbone.js, jshint linting, and QUnit testing, namespace conventions, bootstrap grid system and media queries, classical inheritance of basic view functions such as afterRender, relied on an event-driven PubSub code design pattern, and created modular helpers to facilitate rapid development.

A led a small team as we transitioned from a startup with two developers with no designer to a corporate environment with a scrum master and UX architect. I received opportunities to work on UX, Motion Graphics for an ad campaign, as well as backend development in Python.

The majority of this codebase is locked away in a private repository, but I can share a few small samples, this screenshot shows the general structure of the backbone powered client app. Some interesting code I recognize here is the Version:Changed, which is linked to a continuous integration method that does a poll every few minutes to check a config file if an updated version of the app has been pushed to production recently and creates an overlay that nudges the user to refresh.

The backend was a RESTful API developed in Python with Flask, which was enjoyable to work with, since it serves json inside of queries, meaning I could make dynamically different calls on the frontend without touching backend or setting up new endpoints for specific payload nuances.



Code Sample: CalendarHelper.js

This code sample was an interactive calendar tool that I created for account managers to quickly check the health of their accounts for the month at a glance. Simply call the method with an DOM element and a collection, and it will render a calendar that adds different class to past, future, current day, and a modular filter class based on a passed boolean function.


var time = require('../helpers/date-helper');
var monthTemplate = require('../templates/cal-month-template');
var CalMonthCellView = require('../views/cal-month-cell-view');
var Collection = require('../collections/collection');

module.exports = function(_el, options){
    this.initialize = function(_el, options){
        this.options = options || {};
        this.collection = options.collection || false;
        this.el = _el;
        this.el.addClass('cal');
        this.today = new moment();
        this.currentTime = this.options.currentTime || this.today.clone();
        this.days_of_week = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
        this.highlight || this.options.highlight || "";
        this.filterClass = this.options.filter || this.blankFunction;
        this.onSelect = this.options.onSelect || this.blankFunction;
        this.el.html( monthTemplate() );
        this.createMonthCalendar(this.currentTime);
        this.setupDOMEvents();
        if(this.highlight){
            this.initialTime = new moment(this.highlight, "M/D/YYYY");
        }
    }

    this.blankFunction = function(D){
        return "";
    };

    this.createMonthCalendar = function(_date){
        var w = 7;
        var i_today = this.today.format('D');
        var last = _date.clone().endOf('month');
        var first = _date.clone().startOf("month");
        var first_d = first.format('d');
        var last_d = last.format('d');
        var last_D = last.format('D');
        var M = _date.format("M");
        var D = 0;
        var d = 0;
        var day_of_week;
        var cal = [];
        var monthString = _date.format("MMMM");
        var yearString = _date.format("YYYY");
        var linkString = function( D ){
            var _linkString = M + "/" + D + "/" + yearString;
            return _linkString;
        };
        //headers
        cal.push('<tr class="dayRow">');
        cal.push('<td>S</td><td>M</td><td>T</td><td>W</td>');
        cal.push('<td>TH</td><td>F</td><td>S</td>');
        for( var i = 0; i < 42; i++){
            d = i%w;
            if(d === 0){
                cal.push("<tr>");
            }
            if( i >= first_d && D < last_D ){
                D++;
                cal.push("<td class='cal-cell cal-");
                cal.push(this.days_of_week[d]);
                cal.push(" ");
                cal.push("cal-");
                cal.push(linkString(D));
                cal.push(this.filterClass(D));
                cal.push(" ");
                cal.push(this.isTodayClass(D));
                cal.push(" ");
                cal.push(this.isPastClass(D));
                cal.push(" cal-current-month'>");
                cal.push(   "<a class='cal-link ");
                cal.push( this.highlightClass( linkString(D), this.highlight) );
                cal.push("' href='");
                cal.push(linkString(D));
                cal.push("'>");
                cal.push(D);
                cal.push("<div class='cellCircle'></div>");
                cal.push("</a>");
                cal.push("<div class='cellContent'></div>");
                cal.push("</td>");
            } else {
                cal.push("<td class='cal-" + this.days_of_week[d] + "'></td>");
            }
            if(d === 6){
                cal.push("</tr>");
            }
        }
        var calString = cal.join("");
        this.el.find('tbody').html( calString );
        this.el.find('.cal-month').text( monthString );
        this.el.find('.cal-year').text( yearString );
        this.populateCalendar();
    };

    this.isTodayClass = function( D ){
        var currentMonth = this.currentTime.format("M");
        var todayMonth = this.today.format("M");
        var todayDay = parseInt( this.today.format("D") );
        var isToday = "";
        if ( currentMonth === todayMonth ){
            if( D === todayDay ){
                isToday = "cal-today";
            }
            else {
                isToday = "cal-future";
            }
        } else {
            isToday = "";
        }
        return isToday;
    };

    this.highlightClass = function(linkString, highLightString){
        var _highLightClass = "";
        if(highLightString && linkString === highLightString){
            _highLightClass = "cal-highlight";
        }
        return _highLightClass;
    };

    this.isPastClass = function( D ){
        var todayDay = parseInt( this.today.format("D") );
        var currentMonth = parseInt(this.currentTime.format("M"));
        var todayMonth = parseInt(this.today.format("M"));
        var currentYear = parseInt(this.currentTime.format("YYYY"));
        var todayYear = parseInt(this.today.format("YYYY"));
        var isPast = "";

        if( D < todayDay && currentMonth <= todayMonth && currentYear <= todayYear){
            isPast = "cal-past";
        }
        else {
            isPast = "";
        }
        return isPast;
    };

    this.setupDOMEvents = function(){
        this.month_el = this.el.find(".cal-month");
        this.year_el = this.el.find(".cal-year");
        this.next_el = this.el.find(".cal-next");
        this.prev_el = this.el.find(".cal-prev");
        this.link_el = this.el.find(".cal-link");

        this.next_el.click((e) => {
            e.preventDefault();
            localStorage.removeItem("scrollID");
            self.currentTime.add(1, 'M');
            self.refresh();
        });

        this.prev_el.click((e) => {
            e.preventDefault();
            localStorage.removeItem("scrollID");
            self.currentTime.subtract(1, 'M');
            self.refresh();
        });

        this.link_el.click(function(e){
            e.preventDefault();
        });

    };

    this.populateCalendar = function(){
        var activeDates = {};
        this.collection.each(function(model){
            var date = model.get("date");
            if(!activeDates[date]){
                activeDates[date] = new Collection();
            }
            activeDates[date].add(model);
        });

        _.each(activeDates, function(dateCollection){
            var model = dateCollection.first();
            var calCell = ".cal-" + model.get("date") + " .cellContent";
            var escaped = calCell.replace(/\//g, "\\/");
            var $el = $(escaped);
            $el.parent().addClass("hasEvents");
            var cellView = new CalMonthCellView();
            cellView.$el = $el;
            cellView.collection = dateCollection;
            cellView.render();
        });
        this.resizeCalendar();
    };

    this.resizeCalendar = function(){
        var cell = $(".cal-cell");
        cell.height(cell.width());
    };

    this.refresh = function(){
        if(this.resetFilters === true){
            this.filterClass = this.blankFunction;
            this.highlightClass = this.blankFunction;
        }
        self.createMonthCalendar( self.currentTime );
    };
    this.initialize(_el, options);
}

An earlier iteration of the admin view. Notice the helpful functions such as sorting and filtering.



Code Sample: ReconciliationView.js

This code sample part of a reconciliation flow that checks whether a worker & a client have submitted similar timecards and attempts to reconcile the conflicts through a series of business logic steps that I won't go to in depth into, but here's a function that collects that errors that will later be used to visually represent the errors in the template. The timecards relied on a document-subscribe replication protocal framework (similar to websockets) called CouchDB, that interacted with javascript with PouchDb


 detectWorkerConflicts: function(shift){
        shift.conflicts = shift.conflicts || [];
        var timeIn_m = new moment(shift.timeIn_UTC);
        var timeOut_m = new moment(shift.timeOut_UTC);
        var clientBill = this.getBillHours(timeIn_m, timeOut_m, shift.break);
        var conflictTimeIn_m = new moment(shift.conflictTimeIn_UTC);
        var conflictTimeOut_m = new moment(shift.conflictTimeOut_UTC);
        var conflictBill = this.getBillHours(
            conflictTimeIn_m,
            conflictTimeOut_m,
            shift.conflictBreak
        );

        if(!shift.timeIn_UTC){
            shift.conflicts.push("time-in-input");
            shift.conflicts.push("time-in-conflict");
            shift.workerConflict = true;
        }

        if(!shift.timeOut_UTC){
            shift.conflicts.push("time-out-input");
            shift.conflicts.push("time-out-conflict");
            shift.workerConflict = true;
        }

        if(shift.conflictTimeOut_UTC && Math.abs(clientBill - conflictBill) >= 1){
            if(shift.timeIn_UTC){
                if(Math.abs(conflictTimeIn_m.diff(timeIn_m, 'minutes')) > 59){
                    shift.conflicts.push("time-in-input");
                    shift.conflicts.push("time-in-conflict");
                    shift.workerConflict = true;
                }
            }

            if(shift.timeOut_UTC){
                if(Math.abs(conflictTimeOut_m.diff(timeOut_m, 'minutes')) > 59){
                    shift.conflicts.push("time-out-input");
                    shift.conflicts.push("time-out-conflict");
                    shift.workerConflict = true;
                }
            }

            if(Math.abs(shift.break - shift.conflictBreak) > 59){
                shift.conflicts.push("break-conflict");
                shift.conflicts.push("break-input");
                shift.workerConflict = true;
            }
        }

        if(shift.SOA && shift.SOA !== shift.conflictSOA){
            shift.conflicts.push('soa-input');
            shift.conflicts.push('soa-conflict');
            shift.workerConflict = true;
        }
        return shift;
    },