Ben Cole

Front-end web ninja with a lot of full-stack experience




I craft elegant solutions to complex problems.

Scroll down to see just a few examples of my recent projects.

Or browse my languages and frameworks or open source contributions.

Chapman University Homepage

A responsive, multimedia web page. Go to www.chapman.edu

HTML5 Video CSS3 Animations jQuery and Ajax Event Tracking Responsive

With two million pageviews annually, this homepage needs to provide a great overview of Chapman University at a glance, and work on a wide varity of devices and browsers.

What I did

I collaborated with our designer on the page concept. I wrote most of the HTML, CSS, and Javascript for the page. I imagined and implemented all of the animations and movement on the page based on the static PSD file provided to me.

Technical considerations

The page is cross-browser compatibile down to Internet Explorer 8, and will at least not fall apart in Internet Explorer 7 (gasp!). Animations and video elements do not load on mobile devices or older browsers. The page is responsive and the layout is optimized for just about any viewport size.

Social Metrics Tracker

An open source WordPress plugin See it WordPress.org

WordPress Plugin PHPUnit Testing Social APIs

What I did

I created a WordPress plugin which connects to the APIs of six different social networks to collect and report the number of times each post has been shared online.

The plugin was presented at the Higher Ed Web national conference in Buffalo NY in 2013, and is now in use by thousands of WordPress sites worldwide.

Technical considerations

The plugin makes use of the WP Cron to spread out API requests. It includes a PHP circuit breaker mechanism to detect problems with APIs, temporarily shut off requests, and retry after a specified amount of time.

Because social networks handle canonical URLs differently, the plugin can collect data from different protocols, subdomains, or alternate post URLs - then aggreate this data together for reporting.

The plugin uses action hooks and post custom fields in order to help other developers to extend or modify functionality in a clean, update-friendly way.

Code Samples

This is the circuit breaker mechanism which is used when making network requests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
        <?php
/***************************************************
* A circuit breaker helper which helps an application gracefully fall back
* when a remote service becomes unavailable. State persists between WordPress
* sessions by using the Transients API.
***************************************************/

class WordPressCircuitBreaker {
  
  // Reset state if no activity for this period of time.
  public $persist_ttl = WEEK_IN_SECONDS; 

  public $identifier = '';
  private $prefix = 'smt_wpcb_';

  public function __construct($identifier, $options = null) {

    $this->identifier = $identifier;

    $this->load();

    // Accept new options
    if ($options['max_failures']) $this->set('max_failures', $options['max_failures']);
    if ($options['time_to_wait']) $this->set('time_to_wait', $options['time_to_wait']);

    // Defaults
    if (!$this->get('max_failures')) $this->set('max_failures', 3); // Failures to trigger offline status
    if (!$this->get('time_to_wait')) $this->set('time_to_wait', 3 * HOUR_IN_SECONDS); // How long between checks of an offline service

  }

  /***************************************************
  * Allow connections through when the service is online,
  * as well as once in a while to check when a service is down.
  ***************************************************/
  public function readyToConnect() {

    // Not offline yet!
    if ($this->get('fail_count') < $this->get('max_failures')) return true;

    // Offline, check if we should allow one attempt
    return ($this->getTime() > ($this->get('last_query_time') + $this->get('time_to_wait')));

  }

  /***************************************************
  * Get some useful information about the service status
  ***************************************************/
  public function getStatusDetail() {
    return array(
      'working'       => $this->get('fail_count') == 0,
      'fail_count'    => $this->get('fail_count'),
      'error_message' => $this->get('error_message'),
      'error_detail'  => $this->get('error_detail'),
      'last_query_at' => $this->get('last_query_time'),
      'next_query_at' => ($this->readyToConnect()) ? $this->getTime() : $this->get('last_query_time') + $this->get('time_to_wait'),
    );
  }

  /***************************************************
  * Application should report each success
  ***************************************************/
  public function reportSuccess() {
    $this->set('fail_count', 0);
    $this->set('last_query_time', $this->getTime());

    $this->save();
  }

  /***************************************************
  * Application should report each failure
  ***************************************************/
  public function reportFailure($message = 'An error occured, but no error message was reported.', $detail='') {
    $this->set('fail_count', $this->get('fail_count') + 1);
    $this->set('last_query_time', $this->getTime());
    $this->set('error_message', $message);
    $this->set('error_detail', $detail);

    $this->save();
  }

  /***************************************************
  * Get a value
  ***************************************************/
  public function get($key) {
    return isset($this->data[$key]) ? $this->data[$key] : false;
  }

  /***************************************************
  * Set a value
  ***************************************************/
  private function set($key, $val) {
    $this->data[$key] = $val;
  }

  /***************************************************
  * Load saved state
  ***************************************************/
  private function load() {
    if (is_multisite()) {
      $this->data = get_site_transient($this->prefix . $this->identifier);
    } else {
      $this->data = get_transient($this->prefix . $this->identifier);
    }
  }

  /***************************************************
  * Write current state
  ***************************************************/
  private function save() {
    if (is_multisite()) {
      set_site_transient( $this->prefix . $this->identifier, $this->data, $this->persist_ttl );
    } else {
      set_transient( $this->prefix . $this->identifier, $this->data, $this->persist_ttl );
    }
  }

  /***************************************************
  * Get the current time
  ***************************************************/
  public function getTime() {
    return current_time( 'timestamp' );
  }
}
?>
        
Explore full source code on Github

Angular JS Calendar

A responsive, MVC based calendar system.

Ruby on Rails REST API Responsive CSS3 Animations

With a Ruby on Rails back-end, and an Angular.js front-end, this application allowed for the creation and display of thousands of campus events. Complete integration with many third party services to provide single sign on, data synchronization, and real-time ticket statistics.

What I did

I worked with team members to create a requirements document and sprint timeline, including tasks such as: object creation and update workflow, third party software integration, and user permissions and roles.

Together with the teams Ruby on Rails specialist, we created the data structure and ORM with Active Record. We planned out integration and synchronization mechanisms to link our database with multiple third party products like College Net Series 25, Active Directory, OrgSync, University Tickets, and others.

I implemented the front-end UI for the Calendar View, List View, and Single Event view of the application. I created animations and transitions for when the user applies filters and changes the view of the application.

The content editor UI was designed to allow authors to both view and change the content on the same page, in order to streamline process -- editing and previewing at the same time:

Below is a code sample showing some of the simple CSS animation helper classes I created to enhance the appearance of Angular's built in behavior for hiding and showing content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
        
$animation-speed: 0.5s;

.transition-slow {
  transition: all ease 0.7s !important;
}

/***************************************************
* ng-show transitions for crossfade
***************************************************/

// ng-show variant
.fade-in { opacity: 1; }
.fade-in.ng-hide { opacity: 0; }
.fade-in.ng-hide-remove.ng-hide-remove-active { transition: all ease $animation-speed; }

.fade-out { opacity: 1; }
.fade-out.ng-hide { opacity: 0; }
.fade-out.ng-hide-add.ng-hide-add-active { transition: all ease $animation-speed; }

// ng-if variant
.fade-in.ng-enter { opacity: 0; }
.fade-in.ng-enter.ng-enter-active { opacity: 1; }
.fade-in.ng-enter { transition: all ease $animation-speed; }

.fade-out.ng-leave { opacity: 1; }
.fade-out.ng-leave.ng-leave-active { opacity: 0; }
.fade-out.ng-leave { transition: all ease $animation-speed; }

/***************************************************
* ng-show transitions for delayed crossfade
***************************************************/

// ng-show variant
.delayed-fade-in { opacity: 1; }
.delayed-fade-in.ng-hide { opacity: 0; }
.delayed-fade-in.ng-hide-remove.ng-hide-remove-active { transition: all ease $animation-speed $animation-speed; }

.delayed-fade-out { opacity: 1; }
.delayed-fade-out.ng-hide { opacity: 0; }
.delayed-fade-out.ng-hide-add.ng-hide-add-active { transition: all ease $animation-speed $animation-speed; }

// ng-if variant
.delayed-fade-in.ng-enter { opacity: 0; }
.delayed-fade-in.ng-enter.ng-enter-active { opacity: 1; }
.delayed-fade-in.ng-enter { transition: all ease $animation-speed $animation-speed; }

.delayed-fade-out.ng-leave { opacity: 1; }
.delayed-fade-out.ng-leave.ng-leave-active { opacity: 0; }
.delayed-fade-out.ng-leave { transition: all ease $animation-speed $animation-speed; }

/***************************************************
* ng-show transitions for slide
***************************************************/

// ng-if variant
.slide-in-40.ng-enter { max-height:0px; overflow: hidden; }
.slide-in-40.ng-enter.ng-enter-active { max-height: 40px; }
.slide-in-40.ng-enter { transition: all ease $animation-speed; }

.slide-out-40.ng-leave { max-height:40px; overflow: hidden; }
.slide-out-40.ng-leave.ng-leave-active { max-height:0px; }
.slide-out-40.ng-leave { transition: all ease $animation-speed; }


/***************************************************
* ng-show transitions for shrink
***************************************************/

// ng-if variant
.scale-in.ng-enter { transform:scale(0,0); }
.scale-in.ng-enter.ng-enter-active { transform:scale(1,1); }
.scale-in.ng-enter { transition: all ease $animation-speed; }

.scale-out.ng-leave { transform:scale(1,1); }
.scale-out.ng-leave.ng-leave-active { transform:scale(0,0); }
.scale-out.ng-leave { transition: all ease $animation-speed; }

/***************************************************
* Special Transitions
***************************************************/

.slide-fade-in-200.ng-enter { max-height:0px; overflow: hidden; opacity: 0; }
.slide-fade-in-200.ng-enter.ng-enter-active { max-height: 200px; opacity: 1; }
.slide-fade-in-200.ng-enter { transition: all ease $animation-speed; }

.slide-fade-out-200.ng-leave { max-height:200px; overflow: hidden; opacity: 1; }
.slide-fade-out-200.ng-leave.ng-leave-active { max-height:0px; opacity: 0; }
.slide-fade-out-200.ng-leave { transition: all ease $animation-speed; }

.pop-in.ng-enter { opacity: 0; transform: scale(1.25,1.25); }
.pop-in.ng-enter.ng-enter-active { opacity: 1; transform: scale(1,1); }
.pop-in.ng-enter { transition: all ease $animation-speed; }

        


Below is a code sample showing an Angular Directive used in the rendering of the Calendar view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
        
angular.module('cu_directives').directive('browseEventCalendar',
           ['H', '$filter', 'SearchFilter', '$location', 'FilterManager',
  function ( H,   $filter,   SearchFilter,   $location,   FilterManager ) {
  return {
    restrict: "E",
    replace: true,
    transclude: false,
    templateUrl: "events/partials/browse_event_calendar.html",
    scope: {
      events: '=events'
    },
    link: function(scope, elem, attrs, controller) {

      /***************************************************
      * Configurables
      ***************************************************/

      // int day of the week, 0-6
      var week_starts_on = 0;

      // Hours 0-23 accepted; time_of_day <= event.start_datetime
      var times_of_day = {
        'morning' : 0,  // -> $cal-morning (must be zero)
        'midday'  : 12, // -> $cal-midday
        'night'   : 17  // -> $cal-night
      };

      // How many events per month correspond to what density color?
      var month_density_colors = {
                         // Zero events      -> $cal-density-0
        'few'     : 75,  // Up to this many  -> $cal-density-1
        'some'    : 150, // Up to this many  -> $cal-density-2
        'many'    : 225, // Up to this many  -> $cal-density-3
                         // More than these  -> $cal-density-4
      }

      // Estimate the height of each calendar box. Used to compute the time of day section heights. 
      var cal_obj_height = 34;


      /***************************************************
      * Variable initialization
      ***************************************************/
      scope.events_hash = {};
      scope.active_row = $location.search()['active_row'];

      var $container = $('#browse-calendar');
      var row_heights = null;

      /***************************************************
      * Scope Functions
      ***************************************************/

      scope.collapseRows = function($event) {
        scope.active_row = null;
        $event.stopPropagation();
      }

      scope.thumbnail = function(event) {
        if (!event || !event.cover_photo) return;
        return {
          'background-image' : 'url('+event.cover_photo.cover_photo.square.url+')',
        }
      }

      scope.goToToday = function() {
        var now = H.getViewTime();

        // Set current_page index
        for (i = 0; i < scope.pages.length; i++) {
          var pointer = scope.pages[i].date;
          if (now.getFullYear() == pointer.getFullYear() && now.getMonth() == pointer.getMonth()) {
            scope.goToPage(i);
            break;
          }
        }

      }

      scope.goToPage = function(page_num) {
        if (scope.pages[page_num] == undefined) return;
        scope.loading_cover = true;

        scope.current_page = page_num;

        buildCalendar(new Date(scope.pages[page_num].date.getFullYear(), scope.pages[page_num].date.getMonth()));

        // Retrieve new events from the server 
        FilterManager.date_range_filter.value.start = buildDateFromString(scope.rows[0][0].date_string);
        FilterManager.date_range_filter.value.end   = buildDateFromString(scope.rows[scope.rows.length-1][6].date_string);
        
        FilterManager.selectFilter(FilterManager.date_range_filter);
        
        scope.active_row = null;
      }

      scope.nextPage = function() {
        scope.goToPage(scope.current_page + 1);
      }

      scope.prevPage = function($event) {
        scope.goToPage(scope.current_page - 1);
      }

      // Helper to determine unique labels in loops
      scope.differentFromLast = function(value) {
        if (value !== scope.last_value_compared) {
          scope.last_value_compared = value;
          return true;
        } else {
          return false;
        }
      }

      // Convert number of events per month to density! 
      scope.monthDensity = function(num) {
        if (num > month_density_colors.many) return 4;
        if (num > month_density_colors.some) return 3;
        if (num > month_density_colors.few) return 2;
        if (num > 0)  return 1;
        return 0;
      }

      // Get the height for a section
      scope.sectionHeight = function(row_index, time_of_day) {
        if (!row_heights) return;
        return row_heights[row_index][time_of_day]+'px';
      }

      /***************************************************
      * Private Functions
      ***************************************************/

      function setTransitionClass(class_name) {
        $container.removeClass('fx');
        $container.addClass(class_name);
      }

      // Build the calendar; input any date object within the month you want to represent! 
      function buildCalendar(date_obj) {

        setTransitionClass('');

        var
        row  = [],
        rows = [],
        current_month = date_obj.getMonth(),
        date = new Date(date_obj.getFullYear(), current_month, 1);

        // Prepend days from last month
        row = padRowWithDays(row, date, current_month);

        while (date.getMonth() === current_month) {

          row.push(newDay(date));
          date.setDate(date.getDate() + 1);

          // Create new row on week start
          if (date.getDay() === week_starts_on) {
            if (row.length) rows.push(row);
            row = [];
          }
        }

        // Append days for next month
        row = padRowWithDays(row, date, current_month);

        // Add last row
        if (row.length) rows.push(row);

        // Assign to scope
        scope.rows = rows;
        buildWeekdayLabels();

        scope.current_date_obj = date_obj;

        setTimeout(function() {
          setTransitionClass('fx');
        }, 10);

      }


      // Returns a new day object
      function newDay(date_obj) {
        var current_month = scope.pages[scope.current_page].date.getMonth();
        var date_string   = buildDateString(date_obj);
        var classes       = (current_month != date_obj.getMonth()) ? 'dimmed' : '';
        var special_dates = scope.$parent.special_dates && scope.$parent.special_dates[date_string];
        if (buildDateString(H.getViewTime()) == date_string) classes = classes + ' today';

        return {
          date:        date_obj.getDate(),
          date_string: date_string,
          labels:      special_dates ? special_dates.map(function(d) {return d.title; } ) : null,
          classes:     classes
        };
      }

      // Sets up the weekday labels
      function buildWeekdayLabels() {
        var weekdays = [];
        for (i = week_starts_on; i < week_starts_on+7; i++) {
          weekdays.push(H.weekdays[i%7][2]);
        }
        scope.weekdays = weekdays;
      }

      // Pad the row with num days (reverse_pad if prepending to beginning of month)
      function padRowWithDays(row, date, current_month) {

        var
        reverse_order = (date.getMonth() == current_month),
        num_to_pad    = countPadDays(date, reverse_order);

        for (i = 0; i < num_to_pad; i++) {
          var new_date = new Date(date.getFullYear(), date.getMonth(), date.getDate());
          var diff = (reverse_order) ? i - num_to_pad : i;
          new_date.setDate(date.getDate() + diff);
          row.push(newDay(new_date));
        }

        return row;
      }

      // Counts how many days we need to add to the row to fill it up. 
      function countPadDays(date, reverse_order) {
        if (reverse_order) {
          // Count backwards in time
          return (date.getDay() >= week_starts_on) ? date.getDay() - week_starts_on : 7 - Math.abs(date.getDay() - week_starts_on);
        } else {
          // Count forwards in time
          return (date.getDay() <= week_starts_on) ? week_starts_on - date.getDay() : 7 - Math.abs(date.getDay() - week_starts_on);
        }
      }

      // Parse the events in to an indexed hash
      function parseEvents() {
        if (scope.events == undefined) return;

        var new_events_hash = {};

        for (i = 0; i < scope.events.length; i++) {
          var time = scope.events[i].start_datetime;

          var date_string = buildDateString(time, true);

          if (new_events_hash[date_string] == undefined) new_events_hash[date_string] = [];

          new_events_hash[date_string].push(scope.events[i]);
        }

        // Calculate height
        calculateRowHeights(new_events_hash);

        scope.events_hash = new_events_hash;
      }

      // Calculate the height of all the rows
      function calculateRowHeights(events_hash) {
        if (scope.rows == undefined) {
          console.log("An error occured trying to calculate row heights because scope.rows was not set.")
          return; 
        } 
        row_heights = scope.rows.map(function(row) {
          return {
            'morning' : calculateSegmentHeight(row, 'morning', events_hash),
            'midday' : calculateSegmentHeight(row, 'midday', events_hash),
            'night' : calculateSegmentHeight(row, 'night', events_hash),
          }
        });
      }

      // Calculate the height of a time of day segment in a row
      function calculateSegmentHeight(row, time_of_day, events_hash) {
        return row.reduce(function(prev_height, cur, i, row) {
          var current_events = events_hash[cur.date_string+'-'+time_of_day];
          var this_height    = (current_events) ? current_events.length * cal_obj_height : 0;
          return Math.max(prev_height, this_height);
        }, 0);
      }

      // Take a date and return a string
      function buildDateString(date, incl_time_of_day) {
        var date_string = date.getFullYear()+'-'+(date.getMonth()+1)+'-'+date.getDate();
        return (!incl_time_of_day) ? date_string : date_string+'-'+getTimeOfDay(date);
      }

      // Take a string and return a date
      function buildDateFromString(input_string) {
        var pieces = input_string.split('-');
        return (pieces.length >= 3) ? new Date(pieces[0],pieces[1]-1,pieces[2]) : null;
      }

      // Get time of day string
      function getTimeOfDay(date) {
        if (date.getHours() >= times_of_day.night) {
          return 'night';
        } else if (date.getHours() >= times_of_day.midday) {
          return 'midday';
        } else if (date.getHours() >= times_of_day.morning) {
          return 'morning';
        } else {
          return 'other';
        }
      }

      // Read the URL param and render the correct calendar (default: today)
      function setMonthFromURLParam() {
        var param = $location.search()['range'];

        if (!param) {
          scope.goToToday();
          return;
        }

        var dates   = $location.search()['range'].split('...');
        var start   = new Date(dates[0]);
        var end     = new Date(dates[1]);
        var middate = new Date((start.getTime() + end.getTime()) / 2);

        if (setPageNumByDate(middate)) {
          buildCalendar(middate);
        }
      }

      // Select the page corresponding to date_obj
      function setPageNumByDate(date_obj) {
        if(!scope.pages) return false;

        var year  = date_obj.getFullYear();
        var month = date_obj.getMonth();

        for (i = 0; i < scope.pages.length; i++) {
          if (scope.pages[i].date.getMonth() != month) continue;
          if (scope.pages[i].date.getFullYear() != year) continue;

          scope.current_page = i;
          return true;
        }
      }

      // Updates the URL params with the current row
      function updateActiveRow() {
        $location.search('active_row', scope.active_row);
      }

      // Updates the pages
      function updateMonthList() {
        if(!scope.$parent.month_counts) return;

        scope.pages = scope.$parent.month_counts.map(function(item) {
          item.date = new Date(item.year, item.month-1); // WARNING: Rails months are 1-12, JS months are 0-11
          return item;
        });

        // If a page is not selected
        if (scope.current_page === undefined) {
          setMonthFromURLParam();
        }
      }


      /***************************************************
      * Scope Watchers
      ***************************************************/
      scope.$watch('$parent.month_counts', updateMonthList, false); // must be before events watcher
      scope.$watch('active_row', updateActiveRow, false);
      scope.$watch('events', parseEvents, false);


      /***************************************************
      * Initialize on page load
      ***************************************************/
      scope.$on('eventsLoaded', function(event, mass) {
        scope.loading_cover = false;
      });


    }
   };
}]);

        

CU WordPress Theme

Built for multimedia content; Looks great on phones, tablets, and even paper. See it on Chapman Magazine

WordPress Theme Responsive Printer Friendly

Adaptive Story Spaces

Long post titles get shortened in order to fit in the layout (See above). Notice in the animation above that when the user mouses over a featured story, the titles expand and collapse so that the item in focus is shown fully.

Additionally, the featured stories can automatically swap out based on trending data provided by my Social Metrics Tracker plugin.

Totally Responsive

It's aesthetically pleasing to read these blog posts whilst on the go:

And even though the layout is built for posts with beautiful featured images, videos, or other embedable media...

It's still printer friendly!

the stories still look great, thanks to CSS Media Queries for print.




Next, View my open source contributions »