Scott Morgan
Web Developer

I’m an online professional, front-end developer, creative technologist, web guru, code monkey, pixel pusher. Call me what you will. Over the past 20+ years, I have designed, developed, project managed, or consulted on hundreds of web sites, web apps, and native apps, for many fortune 500 clients. The best part of it all, is knowing that millions of users each day are interacting with my work.
Amazon Web Services - S3 Console
Software Development Manager, Toronto, Ontario, July 2022 to Present
Alteris Group
Director of Technology, Windsor, Ontario, May 2018 to July 2022
Scott Morgan Consulting
Owner, Manager, Lead Developer, Calgary, Alberta, May 2006 to July 2022
AppColony Inc.
Lead Senior Front End Developer , Victoria, British Columbia, March 2014 to May 2018
Walt Disney Internet Group
Senior/Lead Front-End Software Engineer, North Hollywood, California, September 24, 2007 to March, 2014
Yahoo! Inc.
Senior Platform Engineer, Sunnyvale, California, December 11, 2006 to September 1, 2007
Critical Mass Inc.
Senior Flash Developer and Flash Development Manager, Calgary, Alberta, May 2, 2005 to December 8, 2006
Innovasium Inc.
Senior Solution Developer, Markham, Ontario, February 24, 2003 to April 27, 2005
McGill Multimedia Inc. (formerly Mosaic Inc.)
Senior Interactive Designer / Group Supervisor, Downtown Toronto, May 1998 to February 2003
Ross Roy Communications Inc.
Interactive Media Traffic Coordinator, Detroit Michigan, January 1998 to April 1998
CIMDI
ASP.NET and Macromedia UltraDev 3.0, Toronto, Ontario, December 2000
St. Clair College
Advertising and Graphic Design Program, Windsor, Ontario, 1993 to 1997
Still one of the coolest and most fun projects I have ever worked on. While working at Disney I was asked to work on a "special project" with the Pixar team. I was flown up to San Francisco to meet with the animators (and John Lasseter) about their next movie. At the time it wasn't publically known as Wall-e. Getting the opportunity to even see the inside of the Pixar offices would be enough for most. I got a full tour, met with animators and sound engineers working on the film, and I got to see an incomplete screening of Wall-e. Certain scenes were complete, others were storyboards with non-actors reading the lines. They even asked me for my feedback after the screening.
Fan-boy stuff aside, the real reason I was there was to market this new movie/franchise on Disney.com. At the time, I was one of the lead engineers behind the architecture of Disney.com. The Pixar creative team had some amazing (some might call impossible) and very creative ideas that they pitched to me. Storyboards and everything. Set in space, they wanted Wall-e to roll on to the screen. Start building the individual elements of the website, and somehow along the way mess everything up, literally crash the site. Eve, would then fly on screen, and fix the site transitioning in to the actual site with the Wall-e trailer playing in the video player.
Inside my head I was thinking, this is a website, not an Imax theatre. Something this complex will take hours, if not days to load with the typical home bandwidth at that time. But the devil on my other shoulder was screaming "YES, YOU CAN DO THIS!".
Truth be told, Pixar did the majority of the heavy lifting here. They animated the entire scene and provided me with the raw video. I added a few alpha channels and using cue points triggered events to sync up the video events with the actual components on the site and ensured the transition from the end of the video to the actual site was seamless. We also had to seamlessly flip to a smaller video of Wall-e and Eve that loops and remains on screen until the guest (what Disney calls users) leaves the page. To make all this happen I had to add some hooks into the "lightyear" framework that was running the entire site. These hooks would later be used for future takeovers that the marketing team would assemble.
At the time, typical home bandwidth was not what it is today. The H.264 codec was somewhat new, so with much experimentation, and with the help of edge streaming from both Akamai and Level 3, and some preloading scripts that I wrote we were able to execute a flawless delivery.
On a good day Disney.com at the time was getting up to 25 million hits a day. No pressure!
Thanks to fans on YouTube, the legacy of this takeover still exists. Click play below to see my work from days gone by.
Being a huge Team Canada hockey fan, this was essentially my dream project. Hockey Canada hired me to build a web portal they could use to communicate with the players and their families while they were at the Olympic Winter Games in Sochi, Russia.
Using Wordpress, I built a custom responsive theme that allowed Hockey Canada to quickly post and update player and family calenders. The site was used to inform players of their travel schedule, inform players of their practice, and game schedules. Provide maps of the Olympic Village and how to get to the different arenas. Provided information on area restaurants and attractions. It even included player and roster information for games. And an ecommerce Team Canada store for familiy and friends who were travelling to Sochi.
The 2014 Winter Games had it's fair share of contreversy. There were many threats of terrorist attacks. Part of the site development was emergency information in case there was such an attack. Secret meeting places. Contact information. Articles on safety and protocols. Information specific to the families about getting out of the country. This content could only be seen by certain individuals who could publish it live if such an event happened. I am glad to say this part of my work was never used.
Probably one of the most exciting parts of this project was having the roster before it was released to the general public. I had to publish the roster as Steve Yzerman announced it to the world on TV.
It all started on Saturday, March 21, 2020. The 3rd day of our company wide mandatory work from home protocol. When most were trying to figure out how to continue working remotely, the owner of Alteris Group and myself were discussing how we could help small businesses get back to work. The CARES (Coronavirus Aid, Relief, and Economic Security) Act was just released by the US government, and its small businesses compliance requirements were written like a feature list of our Learn2Go platform. Of course, like most, we still didn't know the scope of the pandemic. What we did know, we had to pivot our development, and quickly.
Learn2Go is a SasS, web-based and mobile training platform that helps companies train and communicate with their employees using formal and informal e-learning activities. At the heart of the platform is the Experience API (xAPI, formerly TinCan API) used to communicate with any capable LRS (Learning Record Store) and/or LMS (Learning Management System).
The CARES act mentioned employee compliance, and training would be the key for any back to work strategy. The US government was offering grants to small businesses who invested in COVID Compliance training for their staff. This was perfect for us, we build compliance training solutions for clients, and we already had a platform to deliver it.
Just over a month later, we launched a mobile app, a progressive web app, and an extensive web-based management portal. The COVID-19 Compliance Coach included 6 micro learning modules built in Articulate Rise, a platform to push out notifications to the entire company, specifc locations, departments or even individuals. It includes a TurboTax like editor for building, and publishing a corporate Risk Response Plan that updates automatically based on the current Community Risk Factor set by an administrator. And lastly, each employee has access to a simple health screening questionnaire that notifies management of potential employee health risks.
<script>
import { format } from 'date-fns';
import { mapState } from 'vuex';
import { queryApi } from '@/services/api.service';
export default {
name: 'RiskStatusPage',
props: {
showProfile: {
type: Boolean,
default: true,
},
},
data() {
return {
mitigationPlan: null,
};
},
components: {
ProfileCard: () => import('@/components/profile-card/profile-card.vue'),
},
created() {
this.getBlogs();
},
methods: {
getBlogs() {
const blogParams = {
per_page: 25,
'query[v2_organization_id_eq]': this.defaultOrganization.data.id,
'query[s]': 'updated_at DESC',
'query[draft_eq]': 'false',
'query[within_title_json_cont]': 'mitigationPlan',
};
queryApi('blogs', blogParams)
.then(({ data }) => {
const blog = data.data[0];
const bodyJson = blog.attributes.body;
if (bodyJson.tab1 && bodyJson.tab2 && bodyJson.tab3 && bodyJson.tab4 && bodyJson.tab5) {
this.mitigationPlan = blog;
}
this.mitigationPlan = blog;
})
.catch((err) => {
if (err && err.response && err.response.data) {
this.$notify({
text: 'An error occurred while fetching the data.',
type: 'error',
});
}
});
},
renderMitigationPlanTab(jsonName, riskToRender = 'low') {
let htmlResponse = '';
const fullRiskLabel = `${riskToRender}Risk`;
this.mitigationPlan.attributes.body[jsonName]['en-us'].slides
.map((slide) => slide.questions[0])
.forEach((question) => {
if (question.published) {
if (question.oneAnswer) {
htmlResponse += `
<p class="risk-content-question m-0 mb-1 p-0"><strong>${question.question}</strong></p>
<div class="risk-content-answer m-0 p-0">${question.answer}</div>`;
} else {
htmlResponse += `
<p class="risk-content-question m-0 mb-1 p-0"><strong>${question.question}</strong></p>
<div class="risk-content-answer m-0 p-0">${question[fullRiskLabel]}</div>`;
}
}
});
return htmlResponse;
},
},
computed: {
...mapState(['currentUser', 'currentMember', 'defaultOrganization', 'currentRiskStatus', 'currentRiskStatusReference']),
riskLevelLastUpdated() {
return format(new Date(this.currentRiskStatusReference.data[0].meta.updated_at), 'MM/dd/yy h:mm a');
},
},
};
</script>
ITC, the largest independent electricity transmission company in the United States, wanted to showcase their current projects with both employees and guests in each of their 4 offices across the midwest. Their main requirements: the delivery must be innovative, simple to use, the content must be dynamic and remotely editable, and very secure (they own the grid, understandable).
Working with CTI we assembled 3 touchscreen kiosks that were installed in a custom designed showcase area in the lobby of ITC offices. A standalone 55” 4k touchscreen, a wall mounted 65” 4k touchscreen, and a wall mounted 3 panel 100” multi-touch/multi-panel display.
At the heart of the platform, a WYSIWYG content management system that allows an administrator to publish content to any individual or all installed kiosks. Administrators can create custom navigation, hierarchy, and content via the custom VueJS components and an extensive publishing mechanism built in Rails that pushes out content bundle updates to the Electron based executables installed on each of the kiosks no matter where they are in off hours.
The kiosks contain 3D animated elements that react to users touch, 4k video, high definition photography, and interactions such as drag and drop, card flipping, multiple choice surveys, live data feeds from ITC’s solar field which powers their offices in Novi Michigan.
Lee Valley is one of Canada's most loyal gardening and custom tool brands. Unfortunately, their website did not exhibit the same standard of quality their products and in-store experience provide.
Using Sitecore, and Sitecore's Commerce addon we built a completely dynamic, and modern ecommerce solution that enhanced their brand. I wrote an extensive SASS framework for the Lee Valley site that used CSS Grid with a Flexbox fallback for modern browsers that did not yet support CSS Grid. Using extensive SASS mixins I was able to build a dynamic grid system that allowed developers to create complex grid layouts that worked cross-browser with minimal code, while older browsers, via css overrides, used a flexbox solution with standard margins and padding support for grid-gaps. The mixins were also forgiving of the injected Sitecore markup that wreaked havoc on CSS selectors when the static layouts were incorporated into the Sitecore templates.
Extensive subtle CSS3 UI animations were used to enhance the overall user experience. Subtle rollovers, and transitions when searching or filtering product cards added context to ajax based content updates.
aside {
@include set-grid-columns(1,12);
@include set-grid-rows(1);
@include create-grid-columns(1);
@include grid-gap(18, 5);
grid-auto-flow: row;
grid-auto-rows: min-content;
overflow:hidden;
@include pixel-to-rem(height, 50);
@include pixel-to-rem(max-height, 50);
transition: all 0.5s ease-in-out;
&.active {
@include pixel-to-rem(height, 400);
@include pixel-to-rem(max-height, 400);
}
@media #{$desktop-only} {
@include set-grid-columns(1,3);
overflow:visible;
height:auto;
max-height: unset;
}
> section {
> h1 {
white-space: nowrap;
}
}
section {
margin-top:0;
background-color:$lightcolor;
@include pixel-to-rem(padding, 10, 18, 18, 18);
@media #{$desktop-only} {
@include pixel-to-rem(padding, 18);
}
h1 {
@include set-grid-columns(1,2);
@include set-grid-rows(1);
@include font-size(14);
@include line-height(34);
color:$basecolor;
font-weight:bold;
text-transform: uppercase;
@include pixel-to-rem(letter-spacing, 1.5);
}
hr {
@include set-grid-columns(1, 2);
@include pixel-to-rem(border-top-width, 1);
@include pixel-to-rem(margin, 15, 0);
border-top-style: solid;
border-top-color: $lightBorderColor;
padding:0;
&:first-of-type {
@include set-grid-rows(2);
}
&:last-of-type {
@include set-grid-rows(20);
}
}
h2 {
@include font-size(12);
@include line-height(34);
@include pixel-to-rem(letter-spacing, 1.5);
color: $basecolor;
font-weight: $SemiBoldWeight;
}
a.lv-button {
@include pixel-to-rem(margin, 10, 0, 0, 0);
@include pixel-to-rem(padding, 5, 25);
}
nav {
ul {
@include unlist();
li {
a,
a:visited {
@include font-size(13);
@include line-height(34);
@include pixel-to-rem(letter-spacing, 1.5);
}
a:hover,
a:active,
a.active {
color: $darklinkcolor;
text-decoration: underline;
font-weight: bold;
}
}
}
}
}
}
@mixin font-size($sizeValue: 16) {
$rem: (($sizeValue) / 10);
font-size: ($sizeValue) + px;
font-size: $rem + rem;
}
@mixin line-height($line-height: 16) {
$rem:(($line-height) / 10);
line-height: $line-height * 1px;
line-height: $rem + rem;
}
@mixin pixel-to-rem($prop: width, $pixel1:false, $pixel2:false, $pixel3:false, $pixel4:false) {
$rem: unquote(0 + 'rem');
$pixel: unquote(0 + 'px');
@if $pixel1 {
$pixel: unquote($pixel1 + 'px');
$rem: unquote((($pixel1) / 10) + 'rem');
}
@if $pixel2 {
$pixel: unquote($pixel + ' ' + $pixel2 + 'px');
$rem: unquote($rem + ' ' + (($pixel2) / 10) + 'rem');
}
@if $pixel3 {
$pixel: unquote($pixel + ' ' + $pixel3 + 'px');
$rem: unquote($rem + ' ' + (($pixel3) / 10) + 'rem');
}
@if $pixel4 {
$pixel: unquote($pixel + ' ' + $pixel4 + 'px');
$rem: unquote($rem + ' ' + (($pixel4) / 10) + 'rem');
}
#{$prop}: $pixel;
#{$prop}: $rem;
}
@mixin create-grid-columns($num, $pre:false, $post:false, $unit: "1fr") {
$cols: '';
$repeatCols: repeat($num, unquote($unit));
@if $pre {
$cols: "#{$pre} " + $cols;
$repeatCols: "#{$pre} " + $repeatCols;
}
@for $i from 1 through $num {
$cols: $cols + ' #{unquote($unit)}';
}
@if $post {
$cols: $cols + " #{$post}";
$repeatCols: $repeatCols + " #{$post}";
}
display:grid;
grid-template-columns: unquote($repeatCols);
@supports (-ms-ime-align: auto) {
grid-template-columns: unquote($cols);
}
@media #{$ie-only} {
grid-template-columns: unquote($cols);
}
}
@mixin set-column-matrix($max: 12) {
@for $i from 1 through $max {
@for $x from 1 through $max {
&.lv-col#{$i}_#{$x} {
@media #{$desktop-only} {
@include set-grid-columns(#{$i}, #{$x});
}
}
}
}
}
@mixin set-row-matrix($max: 12) {
@for $i from 1 through $max {
&.lv-row#{$i} {
grid-row: #{$i} !important;
}
}
}
@mixin set-grid-columns($col, $span:false) {
-ms-grid-column: $col;
-ms-grid-column-span: $span;
@if $span {
grid-column: $col / span $span;
} @else {
grid-column: $col;
}
}
@mixin create-grid-rows($num, $pre:false, $post:false, $unit: "auto") {
$rows: '';
$repeatRows: "";
@if $num > 0 {
$repeatRows: repeat($num, unquote($unit));
}
@if $pre {
$rows: "#{$pre} " + $rows;
$repeatRows: "#{$pre} " + $repeatRows;
}
@for $i from 1 through $num {
$rows: $rows + ' #{unquote($unit)}';
}
@if $post {
$rows: $rows + " #{$post}";
$repeatRows: $repeatRows + " #{$post}";
}
grid-template-rows: unquote($repeatRows);
@supports (-ms-ime-align: auto) {
grid-template-rows: unquote($rows);
}
@media #{$ie-only} {
grid-template-rows: unquote($rows);
}
}
@mixin set-grid-rows($row, $span:false) {
-ms-grid-row: $row;
-ms-grid-row-span: $span;
@if $span {
grid-row: $row / span $span;
} @else {
grid-row: $row;
}
}
@mixin grid-gap($column:false, $row:false) {
@if $column {
@include pixel-to-rem(grid-column-gap, $column);
@supports (-ms-ime-align: auto) {
> * {
@include pixel-to-rem(margin-right, $column);
}
}
@media #{$ie-only} {
> * {
@include pixel-to-rem(margin-right, $column);
}
}
}
@if $row {
@include pixel-to-rem(grid-row-gap, $row);
@supports (-ms-ime-align: auto) {
> * {
@include pixel-to-rem(margin-bottom, $row);
&:last-child {
margin-bottom:0;
}
}
}
@media #{$ie-only} {
> * {
@include pixel-to-rem(margin-bottom, $row);
&:last-child {
margin-bottom:0;
}
}
}
}
}
@mixin ms-grid-auto-row-flow($offset:4, $child-class:'*', $row-count:1, $elements:40) {
$isIE: false;
$column-count:1;
$row-column-count:1;
@media #{$ie-only} {
$isIE: true;
}
@supports (-ms-ime-align: auto) {
$isIE: true;
}
@if $isIE {
@include create-grid-rows($elements, false, false, 'minmax(min-content,auto)');
@for $i from 1 through $elements {
@if $row-column-count == 4 {
$row-count: $row-count + 1;
$column-count: 1;
$row-column-count: 1;
}
> #{$child-class} {
&:nth-of-type(#{$i}) {
-ms-grid-column: #{$column-count};
-ms-grid-row: #{$row-count};
}
}
$column-count: $column-count + 1;
$column-count: $column-count + $offset;
$row-column-count: $row-column-count + 1;
}
}
}
@mixin add-ms-row-count($num-of-elements) {
$isIE: false;
@media #{$ie-only} {
$isIE: true;
}
@supports (-ms-ime-align: auto) {
$isIE: true;
}
@if $isIE {
@for $i from 1 through $num-of-elements {
> *:nth-child(#{$i}) {
-ms-grid-row: $i;
}
}
}
}
@mixin add-ms-column-count($start-num:1, $num-of-elements:4, $column-count:3) {
@for $i from 1 through $num-of-elements {
> *:nth-child(#{$start-num}) {
-ms-grid-column: $column-count * $i / span $column-count;
}
$start-num: $start-num + 1;
}
}
@mixin create-columns($start-num:1, $num-of-elements:4, $column-count:3) {
$column-pos: 1;
@for $i from $column-pos through $num-of-elements {
> *:nth-child(#{$start-num}) {
grid-column: $column-pos / span $column-count;
}
$start-num: $start-num + 1;
$column-pos: $column-count * $i + 1;
}
}
With any SaaS product, you have to market it. With that said, the CovidComply.com marketing site is more than your average brochureware. Not only does the site inform users of the app's features, it allows them to search, sign up, and schedule webinars using GoToWebinar's API. It allows users to dynamically price out licences based on their number of employees. Users can sign up for a 14-day free trial, set up accounts, and unique locations, assign adminstrators, enter credit card information and pay for their annual subscription using a seamless Stripe integration.
$app->post('/create-customer', function (Request $request, Response $response, array $args) {
$body = json_decode($request->getBody());
$stripe = $this->stripe;
// Create a new customer object
$customer = $stripe->customers->create([
'email' => $body->email,
]);
return $response->withJson(['customer' => $customer]);
});
$app->post('/create-subscription', function (Request $request, Response $response, array $args) {
$body = json_decode($request->getBody());
$stripe = $this->stripe;
try {
$payment_method = $stripe->paymentMethods->retrieve(
$body->paymentMethodId
);
$payment_method->attach([
'customer' => $body->customerId,
]);
} catch (Exception $e) {
return $response->withJson($e->jsonBody);
}
// Set the default payment method on the customer
$stripe->customers->update($body->customerId, [
'invoice_settings' => [
'default_payment_method' => $body->paymentMethodId,
],
]);
// Create the subscription
if ($body->freeTrial != true) {
$subscription = $stripe->subscriptions->create([
'customer' => $body->customerId,
'items' => [
[
'price' => $body->priceId,
'quantity' => $body->quantity,
],
],
'add_invoice_items' => [[
'price' => $body->setupId,
]],
'expand' => ['latest_invoice.payment_intent'],
]);
} else {
$trialTimeStamp = strtotime("+14 days");
$subscription = $stripe->subscriptions->create([
'customer' => $body->customerId,
'items' => [
[
'price' => $body->priceId,
'quantity' => $body->quantity,
],
],
'trial_end' => $trialTimeStamp,
'expand' => ['latest_invoice.payment_intent'],
]);
}
return $response->withJson($subscription);
});
What started as a prototype is now a fully functional, React site using the Fluxbone pattern. Fluxbone is a unidirectional data flow pattern that uses Backbone for it’s models, collections and out of the box CRUD support, and the Flux pattern for its Stores and Actions. Using the same restful APIs as the mobile app I was able to share the same tested endpoints and auth in the Employee Web app. The entire site is written in ES6, JSX, and SASS. Moment.js is heavily used to handle timezone logic, date parsing and date and time formatting.
Originally bundled using Ruby’s Middleman and a handful of middleman specific gems. Recently however, I converted the entire site to use Webpack as the build tool. To date the application has been white-labeled for 3 different clients in multiple languages. Other than a few CSS overrides, and some client specific configuration data, the main codebase wasn't touched.
I am currently testing the codebase using JEST, however, like many projects with tight timelines there could be way more tests and I find myself writing tests during our bi-monthly chore days.
const TimeSheetNav = require('components/timesheets/TimeSheetNav.js.es6').default;
const TimeSheetPreview = require('components/timesheets/TimeSheetPreview.js.es6').default;
const PreviousTimeSheetPreview = require('components/timesheets/PreviousTimeSheetPreview.js.es6').default;
const UIStatusDialog = require('components/UIStatusDialog.js.es6').default;
const TimeSheetAction = require('actions/TimeSheetActions.js').default;
const TimeSheetActions = new TimeSheetAction();
const React = require('react');
export default class TimeSheetsView extends React.Component {
constructor(props) {
super(props);
this.state = {
activePage: 'currentTimeSheet',
currentTimeSheet: null,
previousTimeSheet: null,
currentLocation: null,
statuses: [],
noTimesheets: null,
lockActivePage: false
}
}
componentDidMount() {
TimeSheetStore.on('timesheet:approve:success', this._onTimeSheetPunchesApproved);
TimeSheetStore.on('timesheet:approve:error', this._onTimeSheetApprovalError);
}
componentWillUnmount() {
TimeSheetStore.off('timesheet:approve:success', this._onTimeSheetPunchesApproved);
TimeSheetStore.off('timesheet:approve:error', this._onTimeSheetApprovalError);
this.state.activePage = null;
}
_changeLocation(id) {
let currentData = TimeSheetStore.getCurrentTimeSheet(id);
this.setState({
currentTimeSheet: currentData,
previousTimeSheet: TimeSheetStore.getPreviousTimeSheets(id),
currentLocation: TimeSheetStore.getLocationById(id),
activePage: currentData != null ? 'currentTimeSheet' : 'previousTimeSheet'
});
}
_approvePunches(punches) {
if (punches) {
TimeSheetActions.approvePunches(punches);
}
}
_onTimeSheetPunchesApproved(response) {
TimeSheetActions.getTimeSheets();
this.state.statuses.push({
title: "Success",
message: 'Timesheet Approved.',
status: "success",
key:_.uniqueId()
});
this.setState({
statuses: this.state.statuses
});
this.state.lockActivePage = true;
TimeSheetActions.getTimeSheets();
Utils.enableGlobalSpinner();
}
_onTimeSheetApprovalError(error) {
this.state.statuses.push({
title: "Error",
message: 'Something went wrong when approving your timesheet. Please try again.',
status: "error",
key:_.uniqueId()
})
this.setState({
statuses: this.state.statuses
})
}
_closeStatusDialog(props) {
let count = 0;
let idx = this.state.statuses.map(function(status) {
if (props.status.key == status.key) {
return count;
}
count++;
})
if (idx) this.state.statuses.splice(idx, 1);
this.setState({
statuses:this.state.statuses
})
}
render() {
let statusDialogs = [];
for (let i = 0; i < this.state.statuses.length; i++) {
statusDialogs.push(<UIStatusDialog status={this.state.statuses[i]} key={this.state.statuses[i].key} closeDialog={this._closeStatusDialog.bind(this)} />);
}
let timesheetClass = '';
let noTimeSheetClass = 'hide';
if (this.state.noTimesheets) {
timesheetClass = 'hide';
noTimeSheetClass = '';
}
return (
<div>
<section id="timesheet" className={timesheetClass}>
<aside>
<TimeSheetNav
currentSheet={this.state.currentTimeSheet}
previousSheets={this.state.previousTimeSheet}
activePage={this.state.activePage}
onClickHandler={this._onClickHandler.bind(this)}
/>
</aside>
<main>
<TimeSheetPreview
shouldRender={this.state.currentTimeSheet != null}
type="current"
showHeader={true}
data={this.state.currentTimeSheet}
activeState={this.state.activePage == 'currentTimeSheet' ? 'active' : 'hidden'}
user={this.state.user || {}}
punches={this.state.punches || {}}
locations={this.state.locations || {}}
currentLocation={this.state.currentLocation || {}}
onLocationChange={this._changeLocation.bind(this)}
onApprovePunches={this._approvePunches.bind(this)}
/>
<PreviousTimeSheetPreview
shouldRender={this.state.previousTimeSheet != null}
type="previous"
data={this.state.previousTimeSheet}
activeState={this.state.activePage == 'previousTimeSheet' ? 'active' : 'hidden'}
user={this.state.user || {}}
punches={this.state.punches || {}}
locations={this.state.locations || {}}
currentLocation={this.state.currentLocation || {}}
onLocationChange={this._changeLocation.bind(this)}
onApprovePunches={this._approvePunches.bind(this)}
/>
{statusDialogs}
</main>
</section>
<section id="no-timesheets" className={noTimeSheetClass}>
<img src="/images/Timesheet_Icon.svg" id="no-timesheet-icon" />
<h1>You don't have any timesheets to view.</h1>
<p>Check back later to see a breakdown of your hours worked.</p>
</section>
</div>
)
}
}
With Hockey Canada Network, teams at all levels have on-demand access to videos, drills, articles, training plans and more. As a hockey coach myself I had a personal connection to this project. I wore many hats on this project, lead front-end web developer on the marketing site and ecommerce module, front-end web developer on the custom made React and Rails CMS, front-end developer for the in app content pages, and as a minor hockey coach a lot of the content and interaction was tested on me.
The Downtown Calgary BIA wanted to create a mobile first parking assistant. One of the challenges of visiting a large downtown core is finding parking. Using the Google Maps javascript API extensively, the Downtown Calgary Parking app plots both city lots and non-city lots with custom markers, it embeds a street view look at the lot when you click on the marker, and it also pulls in dynamic data from city feeds that show how many spots are available in each city lot. Using the polyline features of the Google Maps API, and other city feeds, street level parking is drawn on to the map, pricing based on the time of day is displayed, and how many spots remain in a given block. Users are also able to search for any address or landmark in downtown calgary and the parking search will centre around that location.
As one of the lead front-end developers on the Disney.com team I was asked to rebuild the current static Disney Insider and D23 blogs in Wordpress. At the same time they also wanted to add a third blog, Disney OMD. Oh My Disney is Disney's lighthearted take on Disney news, gossip, and entertainment. Content written specifically to be shared on social media.
Using a multi-site set up, and three custom Wordpress themes the Disney blog suite was built. Custom Disney theme widgets were built to share content across the three sites. A Disney Music widget, a Disney Movies widget, and a Disney Books widget that linked to the most popular content. As with anything Disney, the themes had to be media rich, accessible to all, responsive, and incorporate the global Disney footer and header that were not built with Wordpress or PHP in mind. The PHP scraped the current header and footer html from the Disney.com homepage, parsed the content and displayed it within the Wordpress theme header and footer includes.