Monday, March 10, 2014

Amazing Builds with Grunt.js

Continuing with my previous post on my new app Urban Champ, I recently optimized my build process using Grunt.js.

Grunt is an amazing build tool which can be used to do a variety of things. Currently I am using Grunt tasks for concatenating and minifying css files, running the RequireJS build optimizer, preprocessing HTML (to do things like point your built HTML at compiled/minified scripts), and image minification. Grunt is also extremely useful for separating your build targets (such as development and production) and then firing up a server on your localhost. I really like the livereload grunt "watch" task which will automatically reload your server as you change source files in your editor.

My hats off to the Grunt team! I will definitely be keeping a close eye on future Yeoman generators using Grunt. This is an extremely powerful tool for all you web devs out there!

Node grunt packages installed:
{
  "name": "UrbanChamp",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-concat": "^0.3.0",
    "grunt-contrib-uglify": "^0.4.0",
    "grunt-contrib-cssmin": "^0.9.0",
    "grunt-contrib-requirejs": "^0.4.3",
    "grunt-contrib-clean": "^0.5.0",
    "grunt-open": "^0.2.3",
    "grunt-contrib-connect": "^0.7.1",
    "grunt-contrib-watch": "^0.5.3",
    "grunt-contrib-livereload": "^0.1.2",
    "grunt-uncss": "^0.2.0",
    "grunt-processhtml": "^0.3.0",
    "connect-modrewrite": "^0.6.3-pre",
    "grunt-contrib-imagemin": "^0.5.0"
  }
}

Gruntfile.js:
'use strict';
var modRewrite = require('connect-modrewrite');
var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet;
var mountFolder = function (connect, dir) {
    return connect.static(require('path').resolve(dir));
};


module.exports = function (grunt) {
    grunt.initConfig({
      clean: {
        build: {
          src: ['build']
        },
        css: {
          src: ['build/App/css/*.css','!build/App/css/tidy.min.css']
        },
        images: {
          src: ['build/App/images/*']
        }
      },
      watch: {
          livereload: {
              files: [
                  'app/App/*.html',
                  '{.tmp,app}/App/css/{,*/}*.css',
                  '{.tmp,app}/App/js/{,*/}*.js',
                  'app/App/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
              ],
              tasks: ['livereload']
          }
      },  
      connect: {
          options: {
              port: 8888,
              // change this to '0.0.0.0' to access the server from outside
              hostname: 'localhost'
          },
          livereload: {
              options: {
                  middleware: function (connect) {
                      return [
                          modRewrite(
             ['!\\.html|\\.js|\\.svg|\\.css|\\.png|\\.jpg$ /index.html [L]']),
                          lrSnippet,
                          mountFolder(connect, '.tmp'),
                          mountFolder(connect, 'app')
                      ];
                  }
              }
          },
          dist: {
              options: {
                  middleware: function (connect) {
                      return [
                          mountFolder(connect, 'build')
                      ];
                  }
              }
          }
      },          
      open: {
          server: {
              path: 'http://localhost:8888'
          }
      },      
      requirejs: {
        dist: {
         options: {
           appDir: 'app',
            baseUrl: 'App/js', 
            optimize: 'uglify',
            preserveLicenseComments: false,        
            mainConfigFile: 'app/App/js/main.js',
            dir: 'build'
         }
        }
      },
      concat: {
        dist: {
          src: ['app/App/css/*'],
          dest: 'build/App/css/tidy.css'
        }
      },
      cssmin: {
          dist: {
            src:'build/App/css/tidy.css',
            dest: 'build/App/css/tidy.min.css'
          }
      },      
      processhtml: {
        dist: {
          files: {
            'build/index.html': ['app/index.html']
          }
        }
      },
      imagemin:{
        dynamic: {
          files: [{
            expand: true,                  // Enable dynamic expansion
            cwd: 'app/App/images',                   // Src matches are relative to this path
            src: ['**/*.{png,jpg,gif}'],   // Actual patterns to match
            dest: 'build/app/images'                  // Destination path prefix
          }]          
        }

      }            
});


// load plugins
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-open');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-processhtml');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-imagemin');


// register at least this one task
grunt.registerTask('build', ['clean:build','requirejs', 'concat','cssmin','processhtml', 'clean:css', 'clean:images', 'imagemin']);
grunt.registerTask('default', ['build']);

grunt.registerTask('server', function (target) {
    if (target === 'dist') {
        return grunt.task.run(['build', 'open:server', 'connect:dist:keepalive']);
    }

grunt.task.run(['open:server', 'connect:livereload:keepalive', 'watch']);
});
};

Thursday, February 27, 2014

Urban Champ: a user contributed sports arena

The past five months, I have been hard at work on my nights and weekends developing what I feel to be the best application concept I have ever dreamed up. The first step, building a beta web app so I can begin gathering user feedback, is almost complete. The app doesn't do too terribly much right now, but I am still infatuated with the concept. Before I bore you with the details, here's the link to my starter project:

http://urbanchamp.parseapp.com/

It currently provides these basic functions:

  • ability to signup as a user
  • ability to create a tennis court with a geocoded address and basic info, and give player feedback on the court
  • ability to view local courts on a google map, and filter them by these attributes
  • ability to create a tennis league or tennis tournament at a given court, and allow flexible match scheduling
  • ability to view and register for tournaments/leagues in your area
  • ability to start a tournament and generate match-up brackets/draws
  • ability to enter match results and advance tournament rounds
  • automatic leveling/scoring of players based on prior match history and level of competition
And most importantly, all of these functions are provided 100% free to the users. As of now, I am currently working on adding the following (if you are interested in helping, please email me!):

  • rankings - automatically tracking player rankings by county, state, and country
  • the "ESPN" scoreboard, a cool graphical effect that displays a "news" ticker of recent match scores in the area
  • match making - a random match making service that couples you with other similarly rated players in your area also looking for a match
  • a mobile app for entering scores and receiving push notifications about upcoming matches

I have always been amazed by these "competitive arenas" in online video games, and it baffles me why we haven't translated this same sort of phenomenon to competitive sports. As I set out to write the interfaces, I will surely be reviewing the same interfaces that kept me addicted for more competition in the old Battle.net (PC) and Halo (XBOX) days.

Now for the back story. The origins of this app go a ways back to my first season of USTA tennis. I was just getting started with competitive tennis, having played a full Summer and Fall season with friends and family before signing up. I had no idea what competitive tennis was like in my area, so I started asking around and looking for some competition. To my disappointment, there were no affordable flex leagues (outside the expensive country club tennis leagues), and it seemed like every league required you to first have a USTA tennis rating before joining. So, being as competitive as I am, I decided to join the USTA and start my first season of USTA tennis. How bad could it be, right?

To my dismay, I was first informed that I must have a team of no less than eight players (because USTA men's matches always consist of two singles matches, and three doubles matches). I was next informed the matches would adhere to a strict schedule, matches would always be scheduled on Fridays, Saturdays, and Sundays (because everyone plays competitive tennis on these days of the week). Not so bad I guessed... I could just change my weekend schedule. When it came time to signup, I was notified via email by the local league coordinator that I must register with my teammates and pay a league team fee of $50, a per player fee of $25, a yearly USTA membership of $44, and a $16 per match fee (on top of purchasing balls and any other equipment). Wow, I thought! That seems a bit excessive for a casual tennis player like myself, just looking for some competition to keep it interesting. I approved the fees with my wife (since it would obviously hit the budget), and decided it was worth the cost to keep my interest peaked and get exercise competing at something I enjoyed.

After I scrambled to get enough players for a team, we all rushed over to tennislink.usta.com to signup before the hard cut-off date. What further ensued (and became my greatest motivation for Urban Champ) was a general disbelief at how poor a nation-wide tennis association website could be implemented. When I first attempted to login to a 10 year-old ASPX form, I was informed my password was incorrect. What?! I just signed up with my USTA issued membership number, how could this be? I attempted to reset the password, but of course the link failed. Finally, I bailed out and called the 1-800 number... "USTA services, can I help you?" "Yes, my password isn't working. Can you help me?" "Well, I will need your membership number," she replied. So I gave her the number and sure enough, "I'm sorry, I'm going to have to pull up our old system to get back with you, it looks like the account isn't pulling up." Five minutes passed, and she finally came back, "Sir, it looks like your membership was registered in 1997, and since membership numbers are re-used per customer, we're going to have to reset the account manually for you". No way!!! You are still re-using accounts issued over 15 years ago!!! I had to trace my memory way back to when my parents had signed me up for a summer tennis camp in 4th grade. For whatever reason, that ALSO required a USTA membership.

After the registration fiasco, I could finally focus on the matches. At the first match, I was quickly informed by a league veteran, "most of the players in this league are sticklers, they'll quickly call you out for the slighest rule infraction". Can't you give them any kind of bad feedback I wondered?  After the match, it was time to enter the scores. I logged back into Tennis Link and somehow managed to find the match scoring page (completely separate from any of the "My Tennis Page" areas of the site and nested on another league page). After finding the page, it requested a unique match number. What match number? Ohhh, so you mean I have to navigate back to the matches page, copy the number, and then paste it in this form so I can ACTUALLY enter the match scores? OK, I sighed as I navigated this labyrinth. Next I was prompted with a somewhat mysterious scoring form. After reading the scoring rules in fine print, I manged to figure out tie break scores should simply be entered as "1-0" or  "0-1" and individual points would not be recorded.

When it came time for the playoffs, I was informed only four of the twenty-two teams in the league would be advancing (not a huge issue, but still it would be nice if this were more flexible). The season ended on a positive note with my team nearly knocking off one of the top teams, so we could at least try again next season, right? Wrong, there were only two men's sessions in the USTA calendar, a winter and spring session only. Summer and Fall sessions were reserved for Juniors, Mixed Doubles, and Flex Leagues (for players with varying ratings). Bummer, it would be a 6 month wait.

After my first USTA tennis league experience, coupled with my competitive video gaming history and a frequent urging from team mates and other tennis acquaintances to use my software experience to build a tennis site where players could freely setup tennis leagues and matches as they pleased, Urban Champ was born.

Stay tuned for more!

RestSharp - using the Json.net serializer

On a recent project, it became apparent we had an issue when our date inputs started magically incrementing after a few saves by the user. We later found the culprit to be the serialization settings in the Rest Sharp C# library. The default RestSharp serializer was changing these DateTimes to adjust for UTC time against the server time, i.e. we would send '2-26-2014 : 4:49 PM' on the client and the RestSharp library would then translate this to '2-26-2014 9:49 PM" to adjust for UTC/GMT time. After several saves by the user you can probably guess what happens.

The fix for us ending up being avoiding the default RestSharp serializer and just reverting to use the Json.net serializer. You can do this by by overriding the request object's JsonSerializer property like so:


            var client = new RestClient("http://localhost:62104/");
            var request = new RestRequest("Product/card/margin", Method.POST);
            request.RequestFormat = DataFormat.Json;
            request.JsonSerializer = new RestSharpJsonNetSerializer();

You will of course need to define the custom Json.net serializer (shown below). The Rest Sharp team has provided this on their help page along with explanations about their move away from the Json.Net serializer.

https://github.com/restsharp/RestSharp/blob/master/readme.txt

You may lose some features of the RestSharp JsonSerializer, but if you are OK with that this approach should work great. Hopefully this helps someone else in the same boat!

            
using System.IO;
using Newtonsoft.Json;
using RestSharp.Serializers;

namespace MySerializerNamespace
{

    /// 
    /// Default JSON serializer for request bodies
    /// Doesn't currently use the SerializeAs attribute, defers to Newtonsoft's attributes
    /// 
    public class RestSharpJsonNetSerializer : ISerializer
    {
        private readonly Newtonsoft.Json.JsonSerializer _serializer;

        /// 
        /// Default serializer
        /// 
        public RestSharpJsonNetSerializer()
        {
            ContentType = "application/json";
            _serializer = new Newtonsoft.Json.JsonSerializer
            {
                MissingMemberHandling = MissingMemberHandling.Ignore,
                NullValueHandling = NullValueHandling.Include,
                DefaultValueHandling = DefaultValueHandling.Include
            };
        }

        /// 
        /// Default serializer with overload for allowing custom Json.NET settings
        /// 
        public RestSharpJsonNetSerializer(Newtonsoft.Json.JsonSerializer serializer)
        {
            ContentType = "application/json";
            _serializer = serializer;
        }

        /// 
        /// Serialize the object as JSON
        /// 
        /// Object to serialize
        /// JSON as String
        public string Serialize(object obj)
        {
            using (var stringWriter = new StringWriter())
            {
                using (var jsonTextWriter = new JsonTextWriter(stringWriter))
                {
                    jsonTextWriter.Formatting = Formatting.Indented;
                    jsonTextWriter.QuoteChar = '"';

                    _serializer.Serialize(jsonTextWriter, obj);

                    var result = stringWriter.ToString();
                    return result;
                }
            }
        }

        /// 
        /// Unused for JSON Serialization
        /// 
        public string DateFormat { get; set; }
        /// 
        /// Unused for JSON Serialization
        /// 
        public string RootElement { get; set; }
        /// 
        /// Unused for JSON Serialization
        /// 
        public string Namespace { get; set; }
        /// 
        /// Content type for serialized content
        /// 
        public string ContentType { get; set; }
    }
}