Walks LLC, a provider of walking tours in Europe, are relaunching their web site. I built a calendar widget for clients to manage their itineraries visually.

Due to the complexity of the client side interactions required, I decided to use Backbone.js as an MVC Framework.

/* global namespace */
var walks = {
  builder: {},
  collections: {
    bookings_details: [],
    events: [],
    events_domains_groups: [
      // maps events to domain groups, aka locations
      // also embedded by cake within events
    ],
    locations: [
      // DomainsGroup
    ],
    stages: [
      // EventsStagePaxRemaining
    ],
    tags: []
  },
  constructors: {
    backbone: {},
    templates: {},
    views: {}
  },
  data: {
    // variable to pass CakePHP webroot across context
    // set in View/Layouts/main
    webroot: walksllc_webroot,
    display_filters: {
      week: {
        first_date: function() {return new Date($(walks.selectors.calendar_range).attr('data-range-start'))},
        last_date: function() {return new Date($(walks.selectors.calendar_range).attr('data-range-end'))}
      },
      events: {}
    },
  },
  init_data: {},
  selectors: {
    locations: '.location-info tbody',
    stages: '.stages',
    events: '.events',
    events_filters: '.events-filters',
    calendar_range: '.calendar-range'
  },
  settings: {
    builder_height: 480,
    location_duration: 3,
    test_client_id: 118,
    test_mode: false,
    url: {
      ajax_url: function(this_url) {
        return walks.settings.url.base + walks.settings.url.app + this_url;
      },
      app: 'elias/',
      base: 'http://dev.walks.org/',
    }
  },
  views: {
    locations: [],
    events: [],
    stages: []
  }
}

walks.builder = (function(){

  /* Convenience functions */
  Date.prototype.yyyymmdd = function() {
    var yyyy = this.getFullYear().toString();
    var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based
    var dd  = this.getDate().toString();
    return Number(yyyy + (mm[1]?mm:"0"+mm[0]) + (dd[1]?dd:"0"+dd[0])); // padding
  };
  Date.prototype.prettyDate = function() {
    var prettyDate =(this.getMonth()+1) + '/' + this.getDate() + '/' + this.getFullYear();
    return prettyDate;
  }
  function toType (obj) {
    return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
  }
  function capitaliseFirstLetter (string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  }
  function logg (message) {
    if (walks.settings.test_mode) {
      console.log(message);
    }
  }

  function initItineraryBuilder () {
    initMVCTypes();
    loadData();
    initUI();
  }

  function initMVCTypes() {
    initMVCLocations();
    initMVCEvents();
    initMVCStages();
    initMVCBookingsDetails();
    initMVCGeneric();
    initTemplating();
  }

  function initTemplating () {
    // TODO: pre-compile Handlebars walks.constructors.templates
    walks.constructors.templates['events'] = Handlebars.compile($('#handlebars-event-template').html());
    walks.constructors.templates['location-ui'] = Handlebars.compile($('#handlebars-location-ui-template').html());
    walks.constructors.templates['stages'] = Handlebars.compile($('#handlebars-stages-template').html());
    walks.constructors.templates['calendar-grid'] = Handlebars.compile($('#handlebars-calendar-grid-template').html());
    walks.constructors.templates['calendar-hours-legend'] = Handlebars.compile($('#handlebars-calendar-hours-legend-template').html());
  }

// custom MVC types

  function initMVCLocations() {
    walks.constructors.backbone.LocationModel = Backbone.Model.extend({
      getCallbackStage: function () {
        return walks.collections.stages.getByCid(this.get('callback_stage_id'));
      }
    });

    walks.constructors.backbone.LocationsCollection = Backbone.Collection.extend({
      model: walks.constructors.backbone.LocationModel,
      initialize: function() {
        this.on('add', function(location_model) {
          mvcAddView(location_model, 'Location', 'locations');
        });
        this.on('change:start_date', function(m) {
          if (m.has('start_date')) {
            m.set({start_date_pretty: m.get('start_date').prettyDate()});
          } else {
            m.set({start_date_pretty: null});
          }
        });
        this.on('change:end_date', function(m) {
          if (m.has('end_date')) {
            m.set({end_date_pretty: m.get('end_date').prettyDate()});
          } else {
            m.set({end_date_pretty: null});
          }
        });
      },
      listForSelect: function (target_location_id) {
        var temp_collection = walks.collections.locations.map(function(c) {
          var temp_id = c.get('id');
          var temp_object;

          temp_object = {
            name: c.get('name'),
            id: temp_id
          }
          if (target_location_id === temp_id) {
            temp_object['selected'] = true;
          }

          return temp_object;
        });
        return temp_collection;
      }
    });

    walks.collections.locations = new walks.constructors.backbone.LocationsCollection();

    walks.constructors.backbone.LocationView = Backbone.View.extend({
      tagName: 'tr',
      className: 'Cant',
      id: 'Stand',
      attributes: 'It',
      collection: walks.collections.locations,
      events: {
        'click .add-location': 'modifyLocationUI',
        'click .location-cancel': 'cancelLocationUI',
        'click .location-edit': 'editLocationUI'
      },
      initialize: function() {
        this.model.on('change', this.render, this);
      },
      model: walks.constructors.backbone.LocationModel,
      render: function(location_model, options) {
        var temp_stage;
        var temp_location = location_model;
        var rendered_template;

        temp_location['collection'] = this.collection.listForSelect(this.model.get('id'));
        this.$el.addClass('location-ui');
        if (this.model.get('edit_mode') === true) {
          rendered_template = walks.constructors.templates['location-ui'](temp_location);
        } else {
          rendered_template = walks.constructors.templates['location-ui'](this.model);
        }

        this.$el.html(rendered_template);
        $(walks.selectors.locations).append(this.$el);

        if (temp_stage = this.getCallbackStage()) {
          this.bindLocationUI(temp_stage);
        }

        toggleDrawer();
        $('a[href="#Locations"]').click();

        return this;
      },
      bindLocationUI: function(callback_stage) {
        var to_date;
        var from_date;

        from_date = new Date(callback_stage.get('datetime'));
        to_date = new Date(from_date);
        to_date.setDate(to_date.getDate() + walks.settings.location_duration);

        $( ".location-ui .from" ).datepicker({
          defaultDate: from_date,
          changeMonth: true,
          numberOfMonths: 1,
          onClose: function( selectedDate ) {
            $( "#to" ).datepicker( "option", "minDate", selectedDate );
          }
        }).val(from_date.prettyDate());
        $( ".location-ui .to" ).datepicker({
          defaultDate: to_date,
          changeMonth: true,
          numberOfMonths: 1,
          onClose: function( selectedDate ) {
            $( "#from" ).datepicker( "option", "maxDate", selectedDate );
          }
        }).val(to_date.prettyDate());
      },
      cancelLocationUI: function() {
        this.model.set({
          callback_stage_id: null,
          edit_mode: null,
          end_date: null,
          start_date: null
        });
        toggleDrawer(this.el, 'closed');
      },
      editLocationUI: function() {
        this.model.set({edit_mode: true});
      },
      getCallbackStage: function() {
        var temp_stage = this.model.getCallbackStage();
        if (temp_stage) {
          return temp_stage;
        } else {
          return false;
        }
      },
      modifyLocationUI: function() {
        var temp_stage;
        var target_location_id = $('.location-select').val();
        var target_location = walks.collections.locations.get(target_location_id);
        
        target_location.set({
          edit_mode: null,
          end_date: new Date($('.location-ui .to').val()),
          start_date: new Date($('.location-ui .from').val())
        });
       
        if (temp_stage = this.getCallbackStage()) {
          temp_stage.addToItinerary();
        }
        this.paintBuilder();
      },
      paintBuilder: function() {
        // on initial launch, model will only have default start_date
        $('.column.location').removeClass('location');
        walks.collections.locations.forEach(function(m) {
          var temp_start_date = m.get('start_date');
          var date_diff = (m.get('end_date') - temp_start_date)/86400000;
          for (var i=0; i < date_diff; i++) {
            $('[data-date="' + (temp_start_date.yyyymmdd() + i) + '"]').addClass('location');
          }
        });
      }
    });
  }

  function initMVCEvents () {
    walks.constructors.backbone.EventModel = Backbone.Model.extend();
    walks.constructors.backbone.EventsCollection = Backbone.Collection.extend({
      model: walks.constructors.backbone.EventModel,
      initialize: function() {
        this.on('add', function(event_model) {
          mvcAddView(event_model, 'Event', 'events');
        });
      }
    });
    walks.collections.events = new walks.constructors.backbone.EventsCollection;

    walks.constructors.backbone.EventView = Backbone.View.extend({
      events: {
        'click a.show-availability': 'showAvailability'
      },
      initialize: function() {
        this.model.on('change', this.render, this);
      },
      model: walks.constructors.backbone.EventModel,
      render: function () {
        var rendered_template = walks.constructors.templates['events'](this.model);
        this.$el.addClass('span6');
        this.$el.html(rendered_template);
        $(walks.selectors.events).append(this.$el);
        return this;
      },
      showAvailability: function(e) {
        builderOpen();
        $('.hide-availability').show();
        walks.data.display_filters.events.id = this.model.get('id');
        repaintBuilder();
      }
    });
  }

  function initMVCStages () {
    walks.constructors.backbone.StageModel = Backbone.Model.extend({
      initialize: function() {
        this.on('change:itinerary', this.updateBookings, this),
        this.set({js_date: new Date(this.get('datetime').replace(/-/g, '/'))});
      },
      addToItinerary: function() {
        var temp_location = this.getLocation();

        if (this.bookedLocation()) {
          this.set({itinerary: true});
          hideAvailability();
          walks.collections.stages.suggestAnotherBooking(this);
          return true;
        } else {
          temp_location.set({
            callback_stage_id: this.cid,
            edit_mode: true
          });
          return false;
        }
      },
      // convenience function to set attributes dynamically
      alterAttribute: function(this_attribute, attribute_value) {
        this.attributes[this_attribute] = attribute_value;
      },
      bookedLocation: function() {
        var this_location = this.getLocation();

        if (!this_location.get('start_date') || !this_location.get('end_date')) {
          return false;
        } else {
          return true;
        }
      },
      initForDisplay: function() {
        var this_date = this.get('js_date');
        var this_event;
        var this_duration;
        var this_hours;
        var this_minutes;
        var this_end_date;

        if (!this.has('js_end_date')) {
          this_event = walks.collections.events.get(this.get('events_id'));
          this_duration = this_event.get('duration');
          this_hours = Math.floor(this_duration/60);
          this_minutes = this_duration%60;
          this_end_date = new Date(this_date);
          this_end_date.setHours(this_date.getHours() + this_hours);
          this_end_date.setMinutes(this_date.getMinutes() + this_minutes);

          this.set({
            event_name: this_event.get('name_long').replace(/-.*/, ''),
            duration: this_duration,
            js_end_date: this_end_date
          });
        }
      },
      getLocation: function() {
        var this_location;
        var temp_location_id;
        var this_event_id = this.get('events_id');
        
        temp_location_id = walks.collections.events_domains_groups.where({
          event_id: this_event_id,
          primary: true
        })[0].get('group_id');

        this_location = walks.collections.locations.get(temp_location_id);

        return this_location;
      },
      updateBookings: function() {
        if (walks.collections.bookings.length === 0) {
          // create new booking
          $.ajax({
            url: walks.settings.url.ajax_url('Bookings/Add/new/ajax/'),
            success: function (data) {
              walks.data['current_booking'] = JSON.parse(data)['booking_id'];
            },
            error: function (data) {
              // TODO: better error handling
              this.model.set({itinerary: false});
            }
          });
        } else {
          // retrieve existing booking
        }

        if (this.get('itinerary') === true) {
          walks.collections.bookings_details.add(this, {merge: true});
        } else {
          walks.collections.bookings_details.remove(this);
        }
      }
    });

    walks.constructors.backbone.StagesCollection = Backbone.Collection.extend({
      comparator: function(stage_model) {
        return stage_model.get('js_date');
      },
      initialize: function() {
        this.on('add', function(stage_obj) {
          mvcAddView(event_model, 'Stage', 'stages');
        })
      },
      model: walks.constructors.backbone.StageModel,
      isStageAvailable: function(test_stage) {
        var overlapping_itinerary_p = true;

        _.forEach(
          walks.collections.stages.where({itinerary: true}),
          function(itinerary_stage) {
            // exclude stages that overlap with anything in itinerary
            if (itinerary_stage.get('js_date') < test_stage.get('js_end_date') && itinerary_stage.get('js_end_date') > test_stage.get('js_date')) {
              overlapping_itinerary_p = false;
            }
          }
        );
        return overlapping_itinerary_p;
      },
      nextAvailableStage: function(search_stage) {
        return _.find(
          walks.collections.stages.availableStages(search_stage),
          function() {
            // get first result
            return true;
          }
        )
      },
      availableStages: function(start_stage) {
        return _.filter(
          walks.collections.stages.where({in_date_range: true}),
          function(each_stage) {
            if (each_stage.get('js_date') > start_stage.get('js_end_date')) {
              return walks.collections.stages.isStageAvailable(each_stage);
            }
          }
        );
      },
      suggestAnotherBooking: function(this_stage) {
        // TODO: handle when nextAvailable returns null
        this.nextAvailableStage(this_stage).set({'suggested': true});
      }
    });

    walks.collections.stages = new walks.constructors.backbone.StagesCollection;

    walks.constructors.backbone.StageView = Backbone.View.extend({
      events: {
        'click a.toggle-itinerary': 'toggleItinerary',
        'click .stage-inner': 'stageClick'
      },
      initialize: function() {
        this.model.on('change', function() {
          this.render();
        }, this);
      },
      model: walks.constructors.backbone.StageModel,
      render: function () {
        var this_date = this.model.get('js_date');
        var this_events_id = this.model.get('events_id');

        this.$el.addClass('stage');
        var display_state_classes = ['itinerary', 'availability', 'suggested', 'in_date_range'];
        for (var i=0; i<4; i++) {
          if (this.model.get(display_state_classes[i])) {
            this.$el.addClass(display_state_classes[i]);
          } else {
            this.$el.removeClass(display_state_classes[i]);
          }
        }
        this.$el.attr('data-events-id', this_events_id);

        this.$el.html(walks.constructors.templates['stages'](this.model));

        var this_selector = [
          walks.selectors.stages,
          ' [data-date="',
          this_date.yyyymmdd(),
          '"] ',
          ' [data-hour-id="',
          this_date.getHours() +
          '"]'
        ].join('');
        $(this_selector).append(this.$el);

        return this;
      },
      addToItinerary: function() {
        this.model.addToItinerary();
      },
      removeFromItinerary: function () {
        this.model.set('itinerary', false);
        repaintBuilder();
      },
      stageClick: function () {
        if (this.model.get('suggested') == true) {
          filterEvents(
            'id',
            _.map(
              walks.collections.stages.availableStages(this.model),
              function(stage_model) {
                return stage_model.get('events_id');
              }
            )
          );

          builderClose();
        }
      },
      toggleItinerary: function (e) {
        e.preventDefault();
        e.stopPropagation();
        if (this.model.get('itinerary') != true) {
          this.addToItinerary();
        } else {
          this.removeFromItinerary();
        }
      }
    });
  }

  function initMVCBookingsDetails () {
    walks.constructors.backbone.BookingsDetailsModel = Backbone.Model.extend({
      initialize: function() {
        this.on('add', this.syncStage, this);
      },
      findStage: function() {
        this.set({
          stage_id: walks.collections.stages.where({
            datetime: this.get('events_datetimes')
          })[0].get('id')
        });
      },
      markStage: function() {
        walks.collections.stages.get(
          this.get('stage_id')
        ).set({
          itinerary: true
        });
      },
      syncStage: function() {
        this.findStage();
        this.markStage();
      }
    });
    
    walks.constructors.backbone.BookingsDetailsCollection = Backbone.Collection.extend({
      initialize: function() {
        this.on('reset', function() {
          this.forEach(function(b) {
            b.trigger('add');
          });
        }, this);
      },
      model: walks.constructors.backbone.BookingsDetailsModel 
    });

    walks.collections.bookings_details = new walks.constructors.backbone.BookingsDetailsCollection;
  }

// abstracted MVC functions
  function mvcAddView (model_object, view_type, view_type_plural) {
    if (!view_type_plural) {
      view_type_plural = view_type.toLowerCase() + 's';
    }
    var temp_view = new walks.constructors.backbone[view_type + 'View']({model: model_object});
    walks.views[view_type_plural].push(temp_view);
  }

  function mvcAddGenericDataType () {
    var model_constructor_name = arguments[0][0];
    var collection_constructor_name = arguments[0][1];
    var collection_instance_name = arguments[0][2];

    walks.constructors.backbone[model_constructor_name + 'Model'] = Backbone.Model.extend({});
    walks.constructors.backbone[collection_constructor_name + 'Collection'] = Backbone.Collection.extend({
      model: walks.constructors.backbone[model_constructor_name + 'Model']
    });
    walks.collections[collection_instance_name] = new walks.constructors.backbone[collection_constructor_name + 'Collection'];
  }

  function initMVCGeneric () {
    var generic_mvcs = [
      ['Tag', 'Tags', 'tags'],
      ['DomainsGroup', 'DomainsGroups', 'domains_groups'],
      ['EventsDomainsGroup', 'EventsDomainsGroups', 'events_domains_groups'],
      ['Bookings', 'Booking', 'bookings']
    ]
    for (var g=0; g < generic_mvcs.length; g++) {
      mvcAddGenericDataType(generic_mvcs[g]);
    }
  }

// controls and presentation behavior
  function builderToggle () {
    if ($('.builder-content').hasClass('open')) {
      builderClose();
    } else {
      builderOpen();
    }
  }

  function builderOpen () {
    if (!$('.builder-content').hasClass('open')) {
      $('.builder-content').addClass('open').show();
      builderAnimate('open');
    } 
  }

  function builderClose () {
    $('.builder-content').removeClass('open').hide();
    builderAnimate('close');
    hideAvailability();
  } 

  function builderAnimate (direction, height) {
    var opacity;
    var width;

    if (direction === 'open') {
      if (!height) {
        height = walks.settings.builder_height;
      }
      width   = 790;      
    } else if (direction === 'close') {
      if (!height) {
        height  = 0;      
        width   = 0;      
      }
    }

    $('.builder').animate({
      width: width,
      height: height,
      opacity: opacity
    });
  }

  function toggleDrawer (element, direction) {
    if (!direction) {
      direction = 'open';
    }

    if (direction === 'open') {
      builderAnimate('open', 650);
    } else {
      builderAnimate('close', walks.settings.builder_height);
    }
  }

  function bindControls () {
    $('body').click(function () {
      var $target_element = $(event.target);

      if ($('.builder-content').hasClass('open')) {
        if ($target_element.parents().index($('.builder')) == -1) {
          if (!$target_element.hasClass('stop') && !$target_element.hasClass('prevent')) {
            builderClose();
          }
        }
      }  
    });

    $('body').on('click', '.prevent', function(e) {
      e.preventDefault();
    }).on('click', '.stop', function(e) {
      e.stopPropagation();
    }).on('click', '.test', function(e) {
      logg(e);
      debugger;
    });

    $('body').on('click', 'a.repaint', function(e) {
      repaintContent($(this).attr('href'));
    });

    $('.calendar-controls').on('click', '.prev', function() {
      var date = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
      date.setDate(date.getDate() - 7);
      setCalendarRange(date);
      repaintBuilder();
    }).on('click', '.next', function() {
      var date = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
      date.setDate(date.getDate() + 7);
      setCalendarRange(date);
      repaintBuilder();
    }).on('click', '.calendar-scale a.btn', function() {
      $(this).button('toggle');
    });

    $('.events-filters').on('click', 'a', function() {
      $('.events-filters a').removeClass('selected');
      var temp_type = $(this).attr('data-filter-type');
      var temp_value = $(this).attr('data-filter-value');
      filterEvents(
          temp_type,
          temp_value
      );
      repaintBuilder();
      $(this).addClass('selected');
    });

    $('.builder').on('click', '.builder-toggle', function() {
      builderToggle();
    }).on('click', '.hide-availability', function() {
      hideAvailability();
    }).on('click', '.book-all-test', function() {
      walks.collections.stages.where({in_date_range: true}).forEach(function(a) {a.set({'itinerary': true})});
    });

    $('.itinerary-drawer .nav-tabs a').click(function (e) {
      e.preventDefault();
      $(this).tab('show');
      toggleDrawer(this);
    });
  }

  function loadData () {
    var temp_object;
    var temp_collection;
    var internal_object_name;
    var internal_collection_name;

    for (var i in walks.init_data) {
      logg(i);
      switch (i) {
        case 'BookingsDetail':
          internal_object_name = 'BookingsDetail';
          internal_collection_name = 'bookings_details';
          break;
        case 'EventsDomainsGroup':
          internal_object_name = 'EventsDomainsGroup';
          internal_collection_name = 'events_domains_groups';
          break;
        case 'EventsStagePaxRemaining':
          internal_object_name = 'Stage';
          internal_collection_name = 'stages';
          break;
        case 'DomainsGroup':
          internal_object_name = 'Location';
          internal_collection_name = 'locations';
          break;
        default:
          internal_object_name = i;
          internal_collection_name = i.toLowerCase() + 's';
      }

      // cake wraps json objects with the same name
      // this section discards it  for syntactical convenience
      // the else statement below protects against it disappearing, but,
      // TODO: if we ever alter the cake feed, this section could be removed entirely
      temp_collection = _.map(walks.init_data[i], function (each_model) {
        if (each_model[i]) {
          temp_object = each_model[i];
          for (var j in each_model) {
            if (i != j) {
              temp_object[j] = each_model[j];
            }
          }
        } else {
          temp_object = each_model[j];
        }
        return temp_object;
      });

      if (temp_collection[0] != undefined) {
        walks.collections[internal_collection_name].reset(temp_collection).forEach(function (m) {
          if ($.inArray(internal_object_name, ['Event', 'Stage', 'Location']) > -1) {
              mvcAddView(m, internal_object_name, internal_collection_name);
          }
        });
      }
    }

    filterEvents();
    $('.events .spin-wheel').hide();
    repaintBuilder();
  }

  function initUI () {
    setCalendarRange();
    initCalendarGrid();
    bindControls();
    addLocationFormToDrawer();
  }

  function addLocationFormToDrawer () {
    var rendered_template = walks.constructors.templates['location-ui']({
      edit_mode: true,
      collection: function() { return walks.collections.locations.listForSelect(); }
    });
    $(walks.selectors.locations).append(rendered_template);
  }

  function initCalendarGrid () {
    var rendered_template;
    var grid_hours = new Array;
    var grid_day;
    var first_date_of_week = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
    var grid_date = new Date;

    for (var j=0; j<24; j++) {
      grid_hours.push(j);
    }

    rendered_template = walks.constructors.templates['calendar-hours-legend']({hours: grid_hours});
    $(walks.selectors.stages).append(rendered_template);

    for (var i=0; i<7; i++) {
      grid_date.setDate(first_date_of_week.getDate() + i);
      grid_day = {
        grid_date: grid_date.yyyymmdd(),
        day_of_week: i,
        hours: grid_hours
      };
      rendered_template = walks.constructors.templates['calendar-grid'](grid_day);
      $(walks.selectors.stages).append(rendered_template);
    }
    setCalendarHeaders();
  }
  
  function setCalendarRange(date) {
    var day_of_week;
    var first_date_of_week = new Date();
    var last_date_of_week;
    var current_week_string;

    if (!date) {
      var date = new Date();
    }

    day_of_week = date.getDay();
    if (day_of_week == 0) {
      first_date_of_week = date;
    } else {
      first_date_of_week.setDate(date.getDate() - day_of_week);
    }
    $(walks.selectors.calendar_range).attr('data-range-start', first_date_of_week);
    last_date_of_week = new Date(first_date_of_week);
    last_date_of_week.setDate(first_date_of_week.getDate() + 7);
    $(walks.selectors.calendar_range).attr('data-range-end', last_date_of_week);

    current_week_string = (first_date_of_week.getMonth() + 1) + '/' + first_date_of_week.getDate() + ' - ' + (last_date_of_week.getMonth() + 1) + '/' + last_date_of_week.getDate();
    $(walks.selectors.calendar_range).html(current_week_string);

    setCalendarHeaders();
  }

  function setCalendarHeaders () {
    for (var i=0; i<7; i++) {
      var column_selector = '.day[data-day-id="' + i + '"]';
      var first_date_of_week = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
      var each_date = new Date(first_date_of_week);
      each_date.setDate(first_date_of_week.getDate() + i);
      var date_string = (each_date.getMonth() + 1) + '/' + (each_date.getDate());
      $(column_selector).attr('data-date', each_date.yyyymmdd());
      $(column_selector + ' .date-header').html(date_string);
    }
  }

  function filterEvents (filter_type, filter_value) {
    $('.events .spin-wheel').show();

    if (filter_value && toType(filter_value) != 'array') {
      filter_value = [filter_value];
    }

    walks.collections.events.forEach(function(event_model, i) {
      if (!filter_value) {
        event_model.set({displayed: true});
      } else {
        event_model.set({displayed: false});
        for (var i=0; i walks.data.display_filters.week.first_date() && stage_model.get('js_date') < walks.data.display_filters.week.last_date()) {
        stage_model.initForDisplay();
        stage_model.set({in_date_range: true});

        // TODO: accept mulitple filters
        if (stage_model.get('events_id') == walks.data.display_filters.events.id) {
          stage_model.set({availability: walks.collections.stages.isStageAvailable(stage_model)});
        }
      }
    });
  }

  function positionCalendarGrid () {
    var filtered_stages = walks.collections.stages.where({availability: true});
    var earliest_stage;
    var offset_hours = 8;
    var offset_px;

    if (filtered_stages.length > 0) {
      earliest_stage = _.min(
        filtered_stages,
        function(stage) {return stage.get('js_date').getHours()}
      );
      offset_hours = earliest_stage.get('js_date').getHours();
    }

    offset_px = offset_hours * -20;
    if (offset_px < -140) {
      offset_px = -140;
    }
    $('.hours').css({
      top: offset_px + 'px',
    });
  }

  function hideAvailability () {
    walks.data.display_filters.events.id = null;
    repaintBuilder();
    $('.hide-availability').hide();
  }

  return {
    initItineraryBuilder: initItineraryBuilder
  }
})();

$(document).ready(function() {
    var opts = {
      lines: 12, // The number of lines to draw
      length: 7, // The length of each line
      width: 4, // The line thickness
      radius: 10, // The radius of the inner circle
      corners: 1, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      color: '#000', // #rgb or #rrggbb
      speed: 1.8, // Rounds per second
      trail: 60, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: 'auto', // Top position relative to parent in px
      left: 'auto' // Left position relative to parent in px
    };
    var target = $('.events .spin-wheel');
    var spinner = new Spinner(opts).spin(target[0]);
});