Spinner

Hurricane Matthew turned into Hackathon

Hurricane Matthew was a deadly category 4 storm affecting most of the Caribbean and south-mid eastern United States. Like most south Floridians, we prepared heavily for the storm by stocking up with supplies and protecting our homes with hurricane shutters. For the better part of a day and a half we braced for this monster storm and hoped for the best. After the devastating events in Haiti and The Bahamas, the storm veered slightly east, enough to spare the South Florida region from the destructive hurricane force winds. Needless to say, we were all very grateful and simultaneously saddened by the people and areas affected. Overall, we were extremely fortunate, aside from some power outages, South Florida fared quite well. 

On the other hand, near miss scenarios can be quite powerful code-generators. Several hours indoors with not much to do can make someone quite productive. Never underestimate the power of boredom, isolation and confinement. 


I decided it was time to update my personal website. My web presence was quite dated and unmaintained. I wanted something simple, modern, mobile friendly, attractive and easy to update. I decided on an SPA architecture and ASP.NET Core for the backend to get familiar with the new .NET paradigm which I have not had the change to use up to this point. I had about 24 hours to produce something useful so I got to work. I hosted the site in Azure since I had a few free credits to spare. Furthermore, for simplicity I didn’t want to have a database. I decided to use a series of json files to drive some of the sections that might require frequent updates. I also wanted to have an easy mechanism to create image galleries. I love photography and have amassed a vast gallery over time. Moreover, for raw storage I created an S3 bucket which provided me an economical and easy way for me to maintain an ever growing list of assets. Additionally, I setup Cloudfront as a CDN to have a fast and distributed endpoint to access media, JavaScript dependencies, CSS and other assets. The resulting CDN is accessible at http://cdn.planetdiego.com

Front-end templating is provided by Template7, a modern JavaScript template engine with Handlebars like syntax. The library is lightweight, feature rich and performant.

The site is composed of multiple sections, several mapping directly to Menu options. For performance and maintainability, most dynamic content is retrieved asynchronously and rendered using the aforementioned templating engine.  To load, render, perform pre-processing and apply handlers, I created a structure like this one to that manages all the templates that need to be rendered.

[
  {
    "name": "projects",
    "sourceSelector": "script#tmpl-projects",
    "destinationSelector": "section#portfolio",
    "dataSource": "/data/projects.json",
    "callback": myApp.sectionHandlers
  },
  {
    "name": "tweets",
    "sourceSelector": "script#tmpl-tweets",
    "destinationSelector": "#tweet-scroller",
    "dataSource": "/getTweets",
    "callback": myApp.rotateTweets
  },
  {
    "name": "quotes",
    "sourceSelector": "script#tmpl-quotes",
    "destinationSelector": "#main-quote",
    "dataSource": "/getQuotes",
    "dataPostProc": myApp.pickQuote
  }
]

The following is an informal version of the implementation but helps illustrate the mechanics.
Let’s assume the structure above is stored at myApp.templates. The code below will iterate through the list of templates to be rendered, retrieve the data and when the data is received, proceed to render the template while executing any pre-processing on the data or UI handlers that your design requires. Simply pass in the necessary callbacks you would like to execute.

Important: The following script depends on Template7 and jQuery

$(function () {
    "use strict";

    var getDataPromise = function (url) {
        return $.getJSON(url);
    };

    var compileTemplates = function (templates) {
        console.log('Compiling Templates');

        var i,
            html,
            compiled = {};

        for (i = 0; i < templates.length; i++) {
            html = $(templates[i].sourceSelector).html();
            compiled[templates[i].name] = Template7.compile(html);
        }

        return compiled;
    };

    var renderTemplate = function (name, template, $el, dataPromise, dataPostProc, callback, onNoData) {

        dataPromise.then(function (data) {
            console.log("Data loaded");

            if (typeof data === 'undefined' || data == null) {
                console.log("Invalid data, cannot render template.");

                if (onNoData && typeof onNoData === 'function') {
                    console.log("Executing no data callback.");
                    onNoData();
                }

                return -1;
            } else {

                // Perform any post processing of data, if needed.
                if (dataPostProc && typeof dataPostProc === 'function') {
                    data = dataPostProc(data);
                }

                // Render template with passed data
                var html = template(data);

                if (typeof $el !== 'undefined') {
                    $el.html(html);
                } else {
                    return html;
                }
            }
        }).then(function (code) {
            if (callback && typeof callback === 'function' && code !== -1)
                callback();
        });
    };

    var renderTemplates = function (templates, compiledTemplates) {

        for (var i = 0; i < templates.length; i++) {
            console.log("Rendering template: " + templates[i].name);

            renderTemplate(
                templates[i].name,
                compiledTemplates[templates[i].name],
                $(templates[i].destinationSelector),
                getDataPromise(templates[i].dataSource),
                templates[i].hasOwnProperty('dataPostProc') ? templates[i].dataPostProc : undefined,
                templates[i].hasOwnProperty('callback') ? templates[i].callback : undefined,
                templates[i].hasOwnProperty('onNoData') ? templates[i].onNoData : undefined
            );
        }
    };

    // Compile Template - Expensive Operation, perform ONLY once.
    var compiledTemplates = compileTemplates(myApp.templates);

    // Render the templates using the template listing provided and compiled templates.
    renderTemplates(myApp.templates, compiledTemplates);
});

The following is an example of one of the templates used to generate the "Adventures" section. It renders a set of tiles with filters. Additionally each tile has metadata that is displayed on a lightbox when a tile is clicked. The lighbox can displayed an embedded video or static image depending on its type (see json source)

<script id="tmpl-projects" type="text/template7">
    <div class='section-block portfolio-block'>
        <div class='container-fluid'>

            <h4 class='pre'>Some of the things I have been up to...</h4>

            <ul class='portfolio-filters'>
                <li>
                    <a data-group='all' href='#' class='active'>All</a>
                </li>
                {{#categories}}
                <li>
                    <a data-group='{{id}}' href='#'>{{name}}</a>
                </li>
                {{/categories}}
            </ul>

            <ul class='portfolio-items'>

                {{#projects}}
                <li data-groups='{{arrStr categories}}'>
                    <div class='inner'>
                        <img src='{{catImageUrl}}' alt='{{title}}'>
                        <a href='#projpopup-{{@index}}' class='overlay has-popup'>
                            <h4>{{title}}</h4>
                            <p>{{type}}</p>
                        </a>
                    </div>
                    <div id='projpopup-{{@index}}' class='popup-box zoom-anim-dialog mfp-hide'>
                        <figure>
                            {{#js_compare "this.type === 'Video'"}}
                            <!--project popup video-->
                            <iframe width='560' height='315' src='{{popupMediaUrl}}' allowfullscreen></iframe>
                            {{else}}
                            <!--project popup image-->
                            <img src='{{#if popupMediaUrl}}{{popupMediaUrl}}{{else}}{{catImageUrl}}{{/if}}' alt='{{title}}'>
                            {{/js_compare}}
                        </figure>
                        <div class='content'>
                            <!--project popup title-->
                            <h4>{{#if popupTitle}}{{popupTitle}}{{else}}{{title}}{{/if}}</h4>
                            <!--project popup description-->
                            <p>{{description}}</p>
                            {{#if url}}
                            <a href='{{url}}' class='view-more' target='_blank'>See more</a>
                            {{/if}}
                        </div>
                    </div>
                </li>
                {{/projects}}

            </ul>
        </div>
    </div>
</script>

The template above requires the use of a Helper (arrStr) which renders a JavaScript array as string. The section uses Shuffle.js which structures the categories for filtering using that syntax. Fortunately, that is easily accomplished as follows:

// Array to string
Template7.registerHelper('arrStr', function (arr) {
    var str = '\"' + arr.join('\",\"') + '\"';
    // And return joined array
    return '[' + str + ']';
});

Finally, the data used for the template comes from http://cdn.planetdiego.com/web/projects/projects.json

No comments: