2.28.2012

Nomination mobile - part 1



This post its a part of a series about creating an app with node.js and express for Facebook... this time for mobile browsers

Version en español

Desktop version:


Part 10


Now that we have our beta version of nomination, lets create the mobile version so the mobile users can see it and we will help us later for pass this to phonegap


In this case we will use jquery-mobile (jqm) to create our mobile layouts
Our initial mockup is something like this:

Index


Index


Dashboard


Dashboard


Good, lets create our layout with jade, first lets upload everything we need to work with jquery-mobile to our project, upload the css, js and the images to our public folder in the project, dont forget to update the css to point where the images are in your setup...


Lets start with the "layoutm.jade" that describes the layout for mobile, this file goes into views

!!! 5
//if lt IE 7
  | <html class="no-js ie6 oldie" lang="en">
//if IE 7
  | <html class="no-js ie7 oldie" lang="en">
//if IE 8
  | <html class="no-js ie8 oldie" lang="en">
//if gte IE 9
  | <html class="no-js" lang="en">
head
  meta(charset='utf-8')
  meta(http-equiv="X-UA-Compatible",content="IE=edge,chrome=1")
  title= t('app.name')
  meta(name='description', content='Nomination')
  meta(name='author', content='mrpix')
  meta(name="viewport", content="width=device-width, initial-scale=1")
  link(rel="shortcut icon", href="/images/favicon.ico")
  link(rel="apple-touch-icon", href="/images/apple-touch-icon.png")
  // CSS
  link(rel="stylesheet", href="/stylesheets/jqm.css", type="text/css")
  - if (type === 'dashboard')
    link(rel="stylesheet", href="/stylesheets/dashboard.css", type="text/css")
  - else
    link(rel="stylesheet", href="/stylesheets/index.css", type="text/css")
  script(src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js")
  script
     window.jQuery \|\| document.write("<script src='/javascripts/lib/jquery-1.7.1.min.js'>\\x3C/script>")
  script(src="/javascripts/lib/jqm.js")
body
  != body
  script(defer, src='i18next/i18next.js', type='text/javascript')
  - if (type === 'dashboard')
    script(defer, src="/javascripts/mscript.js")
  script
    var _gaq = _gaq || [];
    _gaq.push(['_setAccount', 'UA-XXXXXXXX-X']);
    _gaq.push(['_trackPageview']);

    (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
    })();
| 

Here we are loading the css and the js for mobile, i18next and our functionality with mscript.js and at the end the google analytics part

OK, lets create the index mobile version in "indexm.jade" also in views folder


- var login = "/login";
- if (invited)
    - login = "/login?invited="+invited;
div(data-role="page", data-theme="d")
    div(data-role="header", data-theme="b")
        h1= t('app.name')

    div(data-role="content")
        table.att_table
            thead
                tr
                    th= t('index.features')
            tbody
                tr
                    td= t('index.feature1')
                tr
                    td= t('index.feature2')
                tr
                    td= t('index.feature3')
                tr
                    td= t('index.feature4')
                tr
                    td= t('index.feature5')
        a(href="#{login}", rel="external")
            img(src="/images/cwf.png")
        - if (error)
            .error #{t(error)}
        img(src="/images/ipn.png", width="90px")
    div(data-role="footer", data-theme="b")
        h4 © Ivan Torres -MrPix


Line 1-3: check for any invited user


4: Create our page with the 'data-role="page"' attribute, with this we tell jqm that this div its a page container with the theme "d"

5-6: The hader div will have the title and "b" theme, the header style in jqm will put the title in the center and as a bar at the top of the page


8-29: the content of the page is defined with `data-role="content"`, in this case its just a table with the lsit of features and our login with facebook btn

30-31: page footer just have the copyright announcement and its rendered with "b" theme, applied with `data-role="footer"`


Sweet, lets update our route manager to test for mobile and if we have a mobile visitor send it to our new layout, lets update "roues/index.js"


Lets load our new module create in our past post, remember to install it with "npm install node-bowser" or update the package.json and do "npm install -d", problems with this let me know in the comments...


Load the module in line 5 and add a variable


bowser = require('node-bowser'),
bt;


In our main route lets use the module to decide with views we need to show


bt = new bowser(req);
if (bt.isMobile() || bt.isTablet()){
    res.render('indexm', 
                { 
                    error : req.param('error'), 
                    type: 'index', 
                    invited: req.param('invited'),
                    layout: 'layoutm'
                }
            );
}else{
    res.render('index', 
                { 
                    error : req.param('error'), 
                    type: 'index', 
                    invited: req.param('invited')
                }
            );
}


If its mobile or tablet we will show the mobile version if not our regular desktop version


Its enough for now, later posts we will add pages and functionality


Fork the code in github:




Greetings

2.23.2012

Creating a module for nodejs

For this post, we are going to create a module for nodes to check the browser type, we are going to check the agent to guess if its touch, tablet, mobile, etc... its not 100% sure because we are only checking the agent but its a start... at the end we are going to upload to npm :)

Version en español

First lets create our directories, the module will be called "node-bowser", the structure of the files/folders will be like this:

node-bowser
  • lib
    • node-bowser.js
  • test
    • main.js
    • test.js
  • index.js
  • license
  • package.json
  • Readme.md
We won't use dependencies for the module but we are going to use vows for development, lets create the "package.json":

{
  "name": "node-bowser",
  "description": "bad! browser detection for mobile, tablet and touch",
  "version": "0.1.0",
  "author": "Ipseitycloud, Ivan Torres ",
  "keywords": ["browser detection ua user-agent mobile table touch"],
  "main" : "lib/node-bowser.js",
  "directories" : { "lib" : "./lib" },
  "devDependencies": {
      "vows" : "0.6.1"
  },
  "repository" : {"type": "git" , "url": "http://github.com/pinguxx/node-bowser.git" },
  "engines": { "node": "> 0.2.0" }
}


We are declaring all the necessary stuff for our module, name, description, author, keywords, which is our main file, code directories, dependencies for development (if we need prod dependencies we can put as dependencies without the dev), the repository if we have one and the node version this will work.

Lets fill out our "index.js" usually we just use this to expose the real module, looks like this:

module.exports = require('./lib/node-bowser');

We are only exporting whatever we got from the file "lib/node-bowser.js", lets see how this file looks:

var NBOWSER;
module.exports = NBOWSER = function(req){
    this._agent = req.header ? req.header('user-agent') : req.headers['user-agent'].toLowerCase();
    this._ismobile = (/iphone|ipod|android|blackberry|opera mini|opera mobi|skyfire|maemo|windows phone|palm|iemobile|symbian|symbianos|fennec/i.test(this._agent));
    this._istablet = (/ipad|android 3|sch-i800|playbook|xoom|tablet|kindle|gt-p1000|sgh-t849|shw-m180s|a510|a511|a100|dell streak|silk/i.test(this._agent));
};
NBOWSER.prototype.isMobile = function(){
    return this._ismobile;
};
NBOWSER.prototype.isTablet = function(){
    return this._istablet;
};
NBOWSER.prototype.isTouch = function(){
    return (/iphone|ipad|ipod|android/i.test(this._agent));
};
NBOWSER.prototype.isDesktop = function(){
    return !(this._ismobile || this._istablet);
};

Line1: We are creating our object var NBOWSER that holds all the functionality that we are going to export


2: we export the object that is the function we define here


3-5: our local variables... first we get the agent from req, could be a regular request object or express req, then lets check if this is mobile or table against our list of browsers that we currently support, we should update this frequently


6-11: this functions are just to know if the agent is mobile or tablet

12-14: in this function we check if the agent is touch, for now we are only checking for iOS and android

15: in this function we check we don't have mobile or tablet then we suppose to have a desktop browser... this is all our functionality, easy right?, lets do some testing with vows


Lets create our test folder and our first file "test.js"

/**
 *
 * node-bowser testing
 *
*/
var vows = require('vows'),
    assert = require('assert'),
    nbowser = require('../lib/node-bowser.js');

exports.test = vows.describe('check user agent').addBatch({
    'check mobile' : {
        topic : function(){
            var req = {headers : {'user-agent' : "iphone"}};
            return new nbowser(req);
        },
        'result its iphone' : function(bowser){
            assert.isTrue(bowser.isMobile());
        },
        'and its not desktop' : function(bowser){
            assert.isFalse(bowser.isDesktop());
        }
    },
    'check tablet' : {
        topic : function(){
            var req = {headers : {'user-agent' : "ipad"}};
            return new nbowser(req);
        },
        'result its tablet' : function(bowser){
            assert.isTrue(bowser.isTablet());
        },
        'and its not desktop' : function(bowser){
            assert.isFalse(bowser.isDesktop());
        }
    },
    'check touch' : {
        topic : function(){
            var req = {headers : {'user-agent' : "iphone"}};
            return new nbowser(req);
        },
        'result its touch' : function(bowser){
            assert.isTrue(bowser.isTouch());
        },
        'and its not desktop' : function(bowser){
            assert.isFalse(bowser.isDesktop());
        }
    },
    'check android' : {
        topic : function(){
            var req = {headers : {'user-agent' : "android"}};
            return new nbowser(req);
        },
        'result its android' : function(bowser){
            assert.isTrue(bowser.isMobile());
        },
        'and its not desktop' : function(bowser){
            assert.isFalse(bowser.isDesktop());
        }
    },
    'check android tablet' : {
        topic : function(){
            var req = {headers : {'user-agent' : "xoom"}};
            return new nbowser(req);
        },
        'result its android tablet' : function(bowser){
            assert.isTrue(bowser.isTablet());
        },
        'and its not desktop' : function(bowser){
            assert.isFalse(bowser.isDesktop());
        }
    }
});

Line 6-8:  load the modules needed


10: declare the suite and add the test cases


11-15: Create a new topic were we generate an object of type request and we add an imaginary agent of type "iphone", then we create our node-bowser object and we return it so the others test get it and test it


16-22: we check the agent is iphone and its not desktop


23...: All the rest of the testing are similar, we just send different agents and check for other things, any doubt let me know in the comments

Our main help us to run the test suite and its quite simple:

#!/usr/bin/env node
/**
 * Test runner
 *
*/
var nbowser = require('./test');
nbowser.test.run();


Here we are just getting the test described in the test file and run them.


License file only have the license used in this case AGPL


The "readme.md" that have a description of the module that we are going to put in github, its a good practice to describe in the readme the functionality so they can actually use it, check the readme in github


Good the module is ready, now lets add this to npm repository, but first lets install npm, its very easy just run this command in the console:

curl http://npmjs.org/install.sh | sh


If you install node in windows it now comes with npm if dont for some reason, check the info


Once we have npm, we have to create first our user:

npm adduser


It will ask for name, password and email, any problem ask me or check the documentation

Before publishing it, we need to check that its actually working as expected and it can be installed locally without problems if not publishing it will be problematic... to test it, go to your root folder of the module and do in the console:
npm install . -g


If everything its ok lets test it, go to another dir and install it:

cd ../some-other-folder
npm install ../my-package

And we use it in any dummy node program, if everything goes ok then we publish it.

In our root folder of the module run this:

npm publish

After some seconds we have a success... and we are set you can see your module in " npmjs.org ", for more information check the documentation for
 developers


We are set, now tell the world about your module and start using it


Fork the code

https://github.com/pinguxx/node-bowser

2.20.2012

Nomination part 10 - last touches


Version en español


This post is part of the series about creating an app with node.js, express for Facebook



Lets put a beta version now, lets integrate a search, a way to invite friends to the app, a logo and lets fix some of the reported bugs, lets go with parts:


1. Lets put a validation so users can only vote once a day, for this lets change how we add the voters, this part was made by Github alejandromg that holds the blog, nodehispano, but what he did overall its when we add a voter, we add an object with the facebook id as the object identifier and inside this the date of the last time the user voted, also i update the validation to search where the user has voted. thanks Alejandro


2. Lets add searching, for this we are going to add a little input text in our header as a form, with jquery we are going to bind to the submit event and with some ajax do the searching and on return show the list of nominations with those terms:

First lets update our "dashboard.jade" in the header part:

form#sf(action="#")
            input.search(type="text", placeholder="Search")


and this at the end to show the results

#dialog-sr(title="#{t('dashboard.search_results')}")
    form
        fieldset#sr


then lets add this to "dashboard.js" to add the route

    /**
     * buscar nominaciones
     */
    app.post('/nominations/search', checkUser, function(req, res){
        var term = req.param('term');
        nominator.findNominationByName( term, function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });


Then update uur controller to add seaches in the db:

/**
 * find nominations by term
 * @term nomination name or term
 * @callback function
 *
 */
NOMINATOR.findNominationByName = function(term, callback){
    //TODO: scape this
    var myregex = RegExp(term);
    Nomination.find({name: myregex},['name', 'id'], callback);
};


Finally put our jquery part in "script.js":

$('#sf').submit(function(){
        var searchTerm = $('#sf').find('.search').val();
        if (searchTerm){
            $('.loading').show();
            //console.log(searchTerm);
            $.post("/nominations/search", { term: searchTerm },
                function(data) {
                    if (data){
                        var datal = data.length;
                        if (datal <1 ){
                            showMsg('dashboard.warning', 'dashboard.notfound');
                            $('.loading').hide();
                            return;
                        }
                        var dialog = $( '#dialog-sr' );
                        var fs = dialog.find('#sr');
                        fs.html('');
                        for (var i=0;i<datal;i++){
                            fs.append('<input type="radio" name="sr" value="'+data[i]._id+'"> '+data[i].name+'<br>');
                        }
                        dialog.dialog('open');
                    }else{
                        showMsg('dashboard.error', 'dashboard.error_searching');
                    }
                    $('.loading').hide();
                }
            ).error(function() { $('.loading').hide(); showMsg('dashboard.error', 'dashboard.error_searching'); });
        }
        return false;
    });

3. We need the users invite other people without nominating them so lets put a link to do that, after search text box lets put a link, catch the "click" event with jquery and show the list of friends to invite and from node write in the wall of the invited user(s)

In the hader of "Dashboard.jade"

div
            a.invite(href="#") Invite


at the end:

#dialog-invite(title="#{t('dashboard.invite_friends')}")
    form#filterformi(action="#")
        input#filterinputi(type="text")
    br
    form
        fieldset
            ol#selectablei


Lets add the route to the "dashboard.js"

/**
     * Invitar amigos
     */
    app.post('/invite', checkUser, function(req, res){
        var usersl = req.param('users');
        var userl = 0;
        var onerror;
        if (usersl instanceof Array){
            userl = usersl.length;
            onerror = function (error) {
                        if (error) { log.debug('error posting on voted user'); return; }
                    };
            for (var i=0;i<userl;i++){
                fb.apiCall(
                    'POST',
                    '/'+usersl[i]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: app._locals.t('dashboard.invited'),
                        name: app._locals.t('dashboard.nominate'),
                        link: url
                    },
                    onerror
                );
            }
        }else{
            onerror = function (error) {
                    if (error) { log.debug('error posting on voted user'); return; }
                };
            fb.apiCall(
                'POST',
                '/'+usersl._id+'/feed',
                {
                        access_token: req.session.user.access_token,
                        message: app._locals.t('dashboard.invited'),
                        name: app._locals.t('dashboard.nominate'),
                        link: url
                },
                onerror
            );
        }
        res.json(true);
    });


And our jquery code:

//creamos el dialog de agregar usuarios
    $( "#dialog-invite" ).dialog({
        autoOpen: false,
        height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Invite friend(s)": function() {
                $('.loading').show();
                var dialog = $(this);
                var users = [];
                var userp;
                $('#selectablei').find('.ui-selected').each(function(key, value){
                    users.push({
                        "_id" : $(value).attr('id'),
                        "name" : $(value).text()
                    });
                });
                var ul = users.length;
                if (ul > 0 && ul <= 1){
                    userp = users[0];
                }else{
                    userp = users;
                }
                $.post("/invite", { users: userp },
                    function(data) {
                        if (data){
                            showMsg('dashboard.warning', 'dashboard.invited');
                            dialog.dialog( "close" );
                        }else{
                            dialog.dialog( "close" );
                            showMsg('dashboard.error', 'dashboard.warning_invited');
                        }
                        $('.loading').hide();
                    }
                ).error(function() { $('.loading').hide(); showMsg('dashboard.warning', 'dashboard.warning_invited'); });
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });

$('.invite').click(function(){
        $( "#dialog-invite" ).dialog( "open" );
    });



4. Update our translation strings in all the folders

5. Update some styles

6. Alejandromg found an error at the moment of finishing a nomination when no users were added, the app hang, we just add an "if" to check the list of users if none we just cancel the nomination

7. See my very amateur gimp logo, its like a small diploma showing that you can win something... options are welcome
8. Add a wiki in github with a smal tutorial to use the app (in spanish only for now, translators are welcome)

9. Update the facebook page, put the logo and some info

10. Close github issues and

11. We have a beta :), lets make noise in facebook and with the friends, hopefully they will enjoy the app as well i did creating it


YAY!!!, we have our app working with node.js, express and some other modules

We have still some bugs so you are welcome to help

If you have ideas for enhancements send them, we will try to add them

Next post we will try to do the mobile version

Use and promote the app.

Fork the code in github:


Greetings 

2.17.2012

Nomination part 9 - routes and ajax


Version en español


This post is part of the series about creating an app with node.js, express for Facebook


Part 4
Part 5
Part 6
Part 7
Part 8
Part 9
Part 10

Ready the ui lets put this to work, first lets create our routes to handle all the possible request from our front-end, we have to handle the nominations, creation, erase, add, end, cancel and... i think its all, all this will be in "dashboard.js":

/**
 * Rutas dashboard
*/
var fb = require('facebook-js'),
    url = 'http://nomination.cloudno.de/';

function findIndexByKeyValue(obj, key, value)
{
    var l = obj.length;
    for (var i = 0; i < l; i++) {
        if (obj[i][key] == value) {
            return i;
  }
 }
 return -1;
}

module.exports = function(app, log){
    var nominator = require('../controllers/nominator.js');

    app.error(function(err, req, res, next){
        log.error('Error: ' + err);
        if (err.message === 'nli'){
            res.redirect('?error='+err.message);
        }else{
            res.send(err.message);
        }
    });

    function checkUser(req, res, next){
        if (req.session.user){
            next();
        }else{
            log.notice('someone try to go directly to dashboard on: ' +
                new Date() );
            next(new Error('nli'));
        }
    }
    /**
     * Dashboard landing
    */
    app.get('/dashboard', checkUser, function(req, res){
        log.notice('landed on dashboard user: ' +
            req.session.user.id + ' on: ' + new Date() );
        var invited = req.param('invited');
        res.render('dashboard', { user: req.session.user, error : req.param('error'), type: 'dashboard', invited: invited });
    });

    /**
     * lista de amigos de facebook
     */
    app.get('/friends', checkUser, function(req, res){
        fb.apiCall('GET', '/me/friends', {access_token: req.session.user.access_token}, function(error, response, body){
            if (error){
                log.debug('error getting friends list:' + error);
                throw new Error('Error getting friends list');
            }
            log.notice('getting friends from user:' + req.session.user.id);
            res.json(body);
        });
    });

    /**
     * lista de mis nominaciones
     */
    app.get('/nominations/mine', checkUser, function(req, res){
        nominator.findMyNominations(req.session.user.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * lista de nominaciones donde vote
     */
    app.get('/nominations/voted', checkUser, function(req, res){
        nominator.findVoted(req.session.user.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * lista de nominaciones donde estoy nominado
     */
    app.get('/nominations/appear', checkUser, function(req, res){
        nominator.findNominated(req.session.user.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * regresar nominacion
     */
    app.get('/nominations/:id', checkUser, function(req, res){
        nominator.findNomination(req.params.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * Crear nominacion
     */
    app.post('/nominations/create', checkUser, function(req, res){
        var nomination = {
            'name' : req.param('name'),
            'owner' : req.session.user.id,
            'endDate' : new Date(req.param('datep')),
            'category' : "cat1",
            'sub_cat' : "sub1",
            'active' : true
        };
        nominator.createNomination(nomination, function(err, doc){
            if (err) { log.debug(err); return; }
            log.notice('nomination '+ req.param('name') +' created by: ' + req.session.user.id );
            res.json(doc);
        });
    });

    /**
     * Borrar nominacion
     */
    app.post('/nominations/erase', checkUser, function(req, res){
        //TODO: erase nomination
        var id = req.param('id');
        nominator.eraseNomination(id, function(err){
            if (err) { log.debug(err); res.json(null); return; }
            log.notice('nomination '+ req.param('name') +' erased by: ' + req.session.user.id );
            res.json(true);
        });
    });

    /**
     * Votar en nominacion
     */
    app.post('/nominations/vote', checkUser, function(req, res){
        //TODO: vote nomination, write in own wall, try to write in other user wall
        var voterid = req.session.user.id;
        var id = req.param('id');
        var userid = req.param('userid');
        //add a friend, try to write to the user wall and local
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            nominator.vote(doc, voterid, userid, function(err, nom){
                if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
                var index = findIndexByKeyValue(nom.users, "_id", req.param('userid'));
                res.json(nom.users[index].votes);
                //post to my wall that i voted
                fb.apiCall(
                    'POST',
                    '/'+voterid+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: 'Vote por "' + nom.users[index].name + '" en "' +
                            nom.name + '" en nomi-nation, vota tu tambien',
                        name: "Votar",
                        link: url + '?invited=' + req.param('id')
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on my wall'); return; }
                        log.notice('posted on current user wall: ' + voterid);
                    }
                );
                //post to the victim
                fb.apiCall(
                    'POST',
                    '/'+nom.users[index]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: 'Vote por ti en "' + nom.name + '" en nomi-nation ' +
                            'vota tu tambien',
                        name: "Votar",
                        link: url + '?invited=' + req.param('id')
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on voted user'); return; }
                        log.notice('posted on the voted user wall: ' + nom.users[index]._id);
                    }
                );
                log.notice('nomination '+ req.param('id') +' voted by: ' + req.session.user.id );
            });
        });
    });

    /**
     * Agregar usuario a nominacion
     */
    app.post('/nominations/adduser', checkUser, function(req, res){
        var users = req.param('users');
        var id = req.param('id');
        //add a friend, try to write to the user wall and local
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            nominator.addUser(doc, users, function(err){
                if (err) { log.debug('error adding users'); res.json(null); return; }
                res.json(true);
                var usersl = req.param('users');
                var userl = 0;
                if (usersl instanceof Array){
                    userl = usersl.length;
                    for (var i=0;i<userl;i++){
                        fb.apiCall(
                            'POST',
                            '/'+usersl[i]._id+'/feed',
                            {
                                access_token: req.session.user.access_token,
                                message: 'Te agregue a "' + doc.name + '" en nomi-nation ' +
                                    'agrega a tus amigos tambien',
                                name: "Votar",
                                link: url + '?invited=' + req.param('id')
                            },
                            function (error, response, body) {
                                if (error) { log.debug('error posting on voted user'); return; }
                                log.notice('posted on the added user wall: ' + usersl[i]._id);
                            }
                        );
                    }
                }else{
                    fb.apiCall(
                        'POST',
                        '/'+usersl._id+'/feed',
                        {
                            access_token: req.session.user.access_token,
                            message: 'Te agregue a "' + doc.name + '" en nomi-nation ' +
                                'agrega a tus amigos tambien',
                            name: "Votar",
                            link: url + '?invited=' + req.param('id')
                        },
                        function (error, response, body) {
                            if (error) { log.debug('error posting on voted user'); return; }
                            log.notice('posted on the added user wall: ' + usersl._id);
                        }
                    );
                }
            });
        });
    });

    /**
     * Borrar usuario de nominacion
     */
    app.post('/nominations/eraseuser', checkUser, function(req, res){
        //just erased, dont notice anyone :(
        var user = req.param('user');
        var id = req.param('id');
        //add a friend, try to write to the user wall and local
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations'); res.json(null); return; }
            if (user === 'eraseme'){
                var index = findIndexByKeyValue(doc.users, "_id", req.session.user.id);
                user = doc.users[index];
            }
            nominator.eraseUser(doc, user, function(err){
                if (err) { log.debug('error erasing nominations'); res.json(null); return; }
                res.json(true);
            });
        });
    });

    /**
     * Terminar nominacion
     */
    app.post('/nominations/end', checkUser, function(req, res){
        //TODO: end the nomination and declare a winner
        var id = req.param('id');
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            var users = doc.users;
            var usersl = doc.users.length;
            var voters = doc.voters;
            var votersl = doc.voters.length;
            var winner = users[0];
            for (var j=1; j<usersl;j++){
                if (winner.votes < users[j].votes){
                    winner = users[j];
                }
            }
            res.json(winner);
            fb.apiCall(
                'POST',
                '/'+req.session.user.id+'/feed',
                {
                    access_token: req.session.user.access_token,
                    message: winner.name + ' gano "' + doc.name + '" en nomi-nation ' +
                        'crea tu propia nominacion',
                    name: "Crear",
                    link: url + '?invited=' + req.param('id')
                },
                function (error, response, body) {
                    if (error) { log.debug('error posting on voted user'); return; }
                    log.notice('posted on the created user wall: ' + req.session.user.id);
                }
            );
            for (var i=0;i<usersl;i++){
                if (users[i]._id == req.session.user.id){ continue; }
                fb.apiCall(
                    'POST',
                    '/'+users[i]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: winner.name + ' gano "' + doc.name + '" en nomi-nation ' +
                            'crea tu propia nominacion',
                        name: "Crear",
                        link: url
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on voted user'); return; }
                        log.notice('posted on the user wall: ' + users[i]._id);
                    }
                );
            }
            nominator.eraseNomination(id, function(err){
                if (err) { log.debug('error erasing nomination'); return; }
                log.notice('nomination '+ req.param('name') +' erased by: ' + req.session.user.id );
            });
            for (var i=0;i<votersl;i++){
                if (voters[i]._id == req.session.user.id){ continue; }
                fb.apiCall(
                    'POST',
                    '/'+voters[i]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: winner.name + ' gano "' + doc.name + '" en nomi-nation ' +
                            'crea tu propia nominacion',
                        name: "Crear",
                        link: url
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on voted user'); return; }
                        log.notice('posted on the user wall: ' + voters[i]._id);
                    }
                );
            }
            nominator.eraseNomination(id, function(err){
                if (err) { log.debug('error erasing nomination'); return; }
                log.notice('nomination '+ req.param('name') +' erased by: ' + req.session.user.id );
            });
        });
    });

};


First lets see our middleware and a little helper function

Line 7-16: "findIndexByKeyValue" this function will help us to find the index of creation object in the array, since we use this a lot we put in his own function


21-28: "app.error" will help us to catch any error the app send and show it to the user

30-38: small middleware to check if the user its logged in, if he is we just call "next()" if not we send the user to the index page with the error


Note on middleware: check that the middleware gets behind of the function that handles the route, you can put any number of middleware divided by ",", for example lets imagine we have "checkotherstuff" too, then the route will be: "checkuser, checkstuff, function..."

Now the routes
42-61: The dashboard its simple, we use our middleware and if everything is good we render the "dashboard.jade", "get friends", here we use the Facebook api to bring the list of friends from the user logged in, if we don't have errors we return the json with the friends array.

66-91: We get the type of list the client is asking, we call the controller and we return the json with the list of nominations array, same thing with all the other functions... another way to do it could be add a ":type" to the route and we end with something like: "/nominations/:type" and with code decide what to call to the controller and send the corresponding json, but for this example we leave like this

138-185: vote for an user, we take the parameters send in the post, the voting user which its in session, nomination id and the user to vote for, we search the nomination, if everything its ok we vote for it, if everything its ok we return the number of votes this user have, we post in the user wall who voted and in the voted wall.


Remaining routes are similar, any doubt ask in the comments


Now lets see how to call all this from our client, all this goes in "script.js"

/* Author: mrpix
*/
var options = {
    ns: { namespaces: ['translation'], defaultNs: 'translation'},
    useLocalStorage: true,
    resGetPath: 'locales/resources.json?lng=__lng__&ns=__ns__',
    dynamicLoad: true
};
$.i18n.init(options, function() {
    //TODO: add more text
});
// expresion para buscar texto que contenga cierto termino
jQuery.expr[':'].Contains = function(a,i,m){
    return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0;
};
// expresion para buscar texto que sea igual a otro
jQuery.extend(jQuery.expr[':'], {
     containsExactly: "$(a).text() == m[3]"
});
//mostrar mensajes modales
function showMsg(title, msg, extra){
    var dialog = $( "#dialog-modal" );
    dialog.attr('title', $.t(title));
    if (extra){
        dialog.find('#msg').text($.t(msg, {winner: extra}));
    }else{
        dialog.find('#msg').text($.t(msg));
    }
    dialog.dialog('open');
}
$(function() {
    //instanciar dialogo
    $( "#dialog-modal" ).dialog({
        autoOpen: false,
        modal: true,
        buttons: {
            Ok: function() {
                $( this ).dialog( "close" );
            }
        }
    });
    //guardamos la lista para futuras referencias
    var list = $('#selectable');
    //funcion para votar por un usuario
    var vote = function(ev){
        ev.preventDefault();
        var a = $(this);
        var tr = a.parent().parent();
        var id = tr.attr('id');
        var nid = $('.details').attr('nid');
        var name = $('.details').find('legend').text();
        $.post("/nominations/vote", { id: nid, userid: id },
            function(data) {
                if (data){
                    var votes = tr.find('.votes');
                    votes.html(data);
                }else{
                    showMsg('dashboard.error', 'dashboard.error_voting');
                }
            }
        ).error(function() {showMsg('dashboard.error', 'dashboard.error_voting'); });
        var list = $('#voted');
        var found = list.find('li:containsExactly('+name+')');
        if (found.length < 1){
            var li = $('<li id="'+nid+'" type="voted"><input type="checkbox" /><label>'+name+'</label></li>');
            list.append(li);
            li.find("label").click(checkOne);
            li.find("input").click(checkOne);
        }
    };
    //funcion para borrar usuario
    var erase = function(ev){
        ev.preventDefault();
        var a = $(this);
        var tr = a.parent().parent();
        var id = tr.attr('id');
        var name = tr.find('.name').text();
        var votes = tr.find('.votes').text();
        var nid = $('.details').attr('nid');
        var user = {
            _id : id,
            name : name,
            votes : votes
        };
        $.post("/nominations/eraseuser", { id: nid, user: user },
            function(data) {
                if (data){
                   tr.remove();
                }else{
                    showMsg('dashboard.error', 'dashboard.error_erasing_user');
                }
            }
        ).error(function() { showMsg('dashboard.error', 'dashboard.error_erasing_user'); });
    };
    //cargando amigos del usuario en session
    function loadUsers(next){
        $.getJSON(next || '/friends', function(data) {
            if (data.data.length > 0){
                $.each(data.data, function(key, value){
                    list.append('<li class="ui-state-default" id="'+value.id+'"><img width="30px" src="https://graph.facebook.com/'+value.id+'/picture"/><a>'+value.name+'</a></li>');
                });
                loadUsers(data.paging.next);
            }else{
                return;
            }
        }).error(function() { showMsg('dashboard.error', 'dashboard.error_friends'); });
    }
    //cargando nominaciones del usuario en session, depende del tipo
    function loadNominations(type){
        $.getJSON('/nominations/'+type, function(data) {
            var list = $('#'+type);
            if (data.length < 1 ){
                //showMsg('dashboard.warning', 'dashboard.warning_zarro');
            }else{
                $.each(data, function(key, value){
                    list.append('<li id="'+value._id+'" type="'+type+'"><input type="checkbox" /><label>'+value.name+'</label></li>');
                });
            }
            list.find("label").click(checkOne);
            list.find("input").click(checkOne);
        }).error(function() { showMsg('dashboard.error', 'dashboard.warning_nominations'); });
    }
    //para q solo se puede seleccionar una nominacion a la vez
    function checkOne(){
        var currentEl = $(this);
        $("input:checked").each(function(){
            $(this).attr('checked', false);
        });
        if (currentEl.is('input')){
            currentEl.attr('checked', true);
            showNomination(currentEl.parent().attr('id'), currentEl.parent().attr('type'));
            return;
        }
        var checkbox = currentEl.siblings('input');
        checkbox.attr('checked', !checkbox.attr('checked'));
        showNomination(currentEl.parent().attr('id'), currentEl.parent().attr('type'));
    }
    //cargar la nominacion y llenar details
    function showNomination(id, type, refresh){
        //TODO: show avatar
        $.getJSON('/nominations/'+id, function(data) {
            if (!data){
                //alert('Du! nominacion ya no existe o termino :(');
                showMsg('dashboard.warning', 'dashboard.warning_erased');
            }
            var details = $('.details');
            details.attr('nid', data._id);
            details.find('legend').html(data.name);
            var daten = new Date(''+data.endDate);
            details.find('.date').html($.t('dashboard.end_date')+' '+ daten.getDate()+'/'+(daten.getMonth()+1)+'/'+daten.getUTCFullYear());
            details.find('.refresh').attr('nid', data._id);
            details.find('.refresh').attr('type', type);
            var ntype = type;
            //console.log(data._id);
            if (ntype === 'appear'){
                $('#end').hide();
                $('#cancel').hide();
                $('#remove').show();
            }else if (type === 'mine'){
                $('#end').show();
                $('#cancel').show();
                $('#remove').hide();
            }else{
                $('#end').hide();
                $('#cancel').hide();
                $('#remove').hide();
            }
            var tbody = details.find('.userst').find('tbody');
            tbody.html('');
            var userl = data.users.length;
            for (var i=0; i<userl;i++){
                var id = data.users[i]._id;
                var name = data.users[i].name;
                var votes = data.users[i].votes;
                var tr = $('<tr id="'+id+'"></tr>');
                tr.append('<td class="pic"><img width="20px" src="https://graph.facebook.com/'+id+'/picture"/></td>');
                tr.append('<td class="name">'+name+'</td>');
                tr.append('<td class="votes">'+votes+'</td>');
                var avote = $('<a class=".vote" href="#">'+$.t('dashboard.vote')+'</a>');
                avote.click(vote);
                var aerase = $('<a class=".erase" href="#">'+$.t('dashboard.erase')+'</a>');
                aerase.click(erase);
                var menu = $('<td></td>');
                menu.append(avote);
                menu.append(' / ');
                menu.append(aerase);
                menu.appendTo(tr);
                tbody.append(tr);
            }
            if (!refresh){
                details.show('slide');
            }
        }).error(function() { showMsg('dashboard.error', 'dashboard.error_showing'); });
    }
    //actualizar la nominacion en contexto
    $('.refresh').click(function(ev){
        ev.preventDefault();
        showNomination($(this).attr('nid'), $(this).attr('type'), 'refresh');
    });
    //cargar las nominaciones y usuarios
    loadNominations('mine');
    loadNominations('appear');
    loadNominations('voted');
    loadUsers(null);
    //viene referenciado, mostrar esa nominacion
    var invited = $('.invited');
    if (invited[0]){
        showNomination(invited.attr('invited'), 'voted');
    }
    //para filtrar la lista de amigos
    $("#filterinput").change( function () {
        var filter = $(this).val();
        if(filter) {
            // this finds all links in a list that contain the input,
            // and hide the ones not containing the input while showing the ones that do
            list.find("a:not(:Contains(" + filter + "))").parent().hide();
            list.find("a:Contains(" + filter + ")").parent().show();
        } else {
            list.find("li").show();
        }
        return false;
    }).keyup( function () {
        // fire the above change event after every letter
        $(this).change();
    });
    //inicializar el seleccionador de fecha
    $( "#datep" ).datepicker();
    //hacer seleccionable la lista de nominaciones
    $( "#selectable" ).selectable();
    //abrir el dialog de nueva nominacion
    $("#nn").click(function(){
        $( "#dialog-new" ).dialog( "open" );
    });
    //abrir el dialogo de agregar usuarios
    $("#am").click(function(){
        $( "#dialog-add" ).dialog( "open" );
    });
    //creamos el dialogo de nueva nominacion
    $( "#dialog-new" ).dialog({
        autoOpen: false,
        height: 300,
  width: 450,
  modal: true,
  buttons: {
            //TODO: put a t string :S
   "Create a nomination": function() {
                var dialog = $(this);
                var name = dialog.find('#name').val();
                var datep = dialog.find('#datep').val();
                $.post("/nominations/create", { name: name, datep: datep },
                    function(data) {
                        var list = $('#mine');
                        var li = $('<li id="'+data._id+'" type="mine"><input type="checkbox" checked="true" /><label>'+data.name+'</label></li>');
                        list.append(li);
                        li.find("label").click(checkOne);
                        li.find("input").click(checkOne);
                        li.find("label").trigger('click');
                        dialog.dialog( "close" );
                    }
                ).error(function() { showMsg('dashboard.error', 'dashboard.warning_creating'); });
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
      var dialog = $(this);
            dialog.find('#name').val('');
            dialog.find('#datep').val('');
  }
 });
    //creamos el dialog de agregar usuarios
    $( "#dialog-add" ).dialog({
        autoOpen: false,
  height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Add friend(s)": function() {
                var dialog = $(this);
                var tbody = $('.details').find('.userst').find('tbody');
                var users = [];
                var userp;
                $('#selectable').find('.ui-selected').each(function(key, value){
                    users.push({
                        "_id" : $(value).attr('id'),
                        "name" : $(value).text(),
                        "votes" : 0
                    });
                });
                var ul = users.length;
                if (ul > 0 && ul <= 1){
                    userp = users[0];
                }else{
                    userp = users;
                }
                var nid = $('.details').attr('nid');
                $.post("/nominations/adduser", { id: nid, users: userp },
                    function(data) {
                        if (data){
                            $.each(users,function(key, value){
                                var id = value._id;
                                var name = value.name;
                                var tr = $('<tr id="'+id+'"></tr>');
                                tr.append('<td class="pic"><img width="20px" src="https://graph.facebook.com/'+id+'/picture"/></td>');
                                tr.append('<td class="name">'+name+'</td>');
                                tr.append('<td class="votes">0</td>');
                                var avote = $('<a class=".vote" href="#">'+$.t('dashboard.vote')+'</a>');
                                avote.click(vote);
                                var aerase = $('<a class=".erase" href="#">'+$.t('dashboard.erase')+'</a>');
                                aerase.click(erase);
                                var menu = $('<td></td>');
                                menu.append(avote);
                                menu.append(' / ');
                                menu.append(aerase);
                                menu.appendTo(tr);
                                tbody.append(tr);
                            });
                            dialog.dialog( "close" );
                        }else{
                            dialog.dialog( "close" );
                            showMsg('dashboard.error', 'dashboard.error_adduser');
                        }
                    }
                ).error(function() { showMsg('dashboard.error', 'dashboard.error_adduser'); });
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });
    //para cancelar nominacion
    $('#cancel').click(function(ev){
        ev.preventDefault();
        var nid = $('.details').attr('nid');
        $.ajax({
            type: 'POST',
            url: '/nominations/erase',
            data: {id : nid},
            success: function(data){
                if (!data){ showMsg('dashboard.error', 'dashboard.error_erasing'); return;}
                showMsg('dashboard.warning', 'dashboard.warning_erasing');
                $('#'+nid).remove();
                while($('#'+nid).length > 0){
                    $('#'+nid).remove();
                }
                $('.details').hide();
            },
            error: function(){
                showMsg('dashboard.error', 'dashboard.error_erasing');
            },
            dataType: 'json'
        });
    });
    //para terminar nominacion
    $('#end').click(function(ev){
        ev.preventDefault();
        var nid = $('.details').attr('nid');
        $.ajax({
            type: 'POST',
            url: '/nominations/end',
            data: {id : nid},
            success: function(data){
                if (!data){ showMsg('dashboard.error', 'dashboard.error_ending'); return;}
                showMsg('dashboard.warning','dashboard.win', data.name);
                $('#'+nid).remove();
                while($('#'+nid).length > 0){
                    $('#'+nid).remove();
                }
                $('.details').hide();
            },
            error: function(){
                showMsg('dashboard.error', 'dashboard.error_ending');
            },
            dataType: 'json'
        });
    });
    //para remover al usuario actual de la nominacion
    $('#remove').click(function(ev){
        ev.preventDefault();
        var uid = $(this).attr('uid');
        var nid = $('.details').attr('nid');
        $.post("/nominations/eraseuser", { id: nid, user: 'eraseme' },
            function(data) {
                if (data){
                    //get the row of the user and erase it
                   $('.details').find('#'+uid).remove();
                }else{
                    showMsg('dashboard.error', 'dashboard.error_removing');
                }
            }
        ).error(function() { showMsg('dashboard.error', 'dashboard.error_removing'); });
    });
    //escuchar por los clicks en votar y borrar
    $('.vote').click(vote);
    $('.erase').click(erase);
});

Line 1-11: we start the i18next

12-19: lets extend jquery functionality to find text that contains or match the term passed

21-30: this will show a modal dialog with errors or announcements to the user so we done use an ugly alert

33-41: initialize our messages dialog

45-94: vote and erase user, first lets prevent the regular behavior with "preventDefault", we get the data from the dom and we make a post with jquery sending the data to "/nominations/vote" or erase, if everything goes ok we put the number of votes or erase the user from the list, in any error we show an error msg

96-107: loading the list of friends, i got an error in opera here, it loads the list anyway, im not sure how to fix this, any idea?

109-122: load the nominations by type and we add them to the left list

124-137: we are just letting the user to select one nomination at the time, once is selected we will show the details for it, we un-select first others selected

139-194: showing the details, load first the data and fill out the details with jquery, we generate the dom with jquery in the for to fill out the table

239-334: add new nomination and add user opens a dialog, in the first one just ask the name of the nomination and the end date, the other one show the list of friends of the user with a filter at the top to search quickly an user... the list load once the user log in to the page so we don't have delay while opening this dialog.

336-396: cancel, remove and end button, cancel just end the nomination and don't declare any winner, remove its to remove yourself for one nomination, in case you don't want to be listed there, if you erase yourself or another user it cannot be added again and end its to declare a winner and end the nomination, all the participants will be notified with a wall post

Cool, this post don't pretend to be a jquery tutorial but if you need help or want a better tutorial about this i can create a post for that.

Also if you need help creating an app in Facebook or how to deploy a node app i can explain that more in depth

With this we have an app with enough functionality, we still need to polish it but for now lets put it in a server and we start to notify to the users to test our beta version

Start nominating your friends in:


If you have feedback please comment it or put it on the facebook page


We are creating a logo, send us your examples we may put yours :)

Next time we will add just some things and we will create our github readme to declare it a beta version ready

Fork the code in github:


Greetings 

2.16.2012

Nomination part 8 - ui


Version en español


This post is part of the series about creating an app with node.js, express for Facebook




Lets see how the ui will end up, lets see the mocks i did on cacoo, this is the index


Dashboard


Good lets try to do them in jade


We are going to use the structure from html5boilerplate with the css, we are going to add some small things at the end i will show all the css added


Lets look at the "index.jade"

#header-container
    header.wrapper.clearfix
        h1.title= t('app.name')
#main-container
    #main.wrapper.clearfix
        table.att_table
            thead
                tr
                    th= t('index.features')
            tbody
                tr
                    td= t('index.feature1')
                tr
                    td= t('index.feature2')
                tr
                    td= t('index.feature3')
                tr
                    td= t('index.feature4')
                tr
                    td= t('index.feature5')
                    td
                        a(href="/login")
                            img(src="/images/cwf.png")
        - if (error)
            .error #{error}
#footer-container
    footer.wrapper
        .ficon
            img(src="/images/ipn.png")


We are adding our title a table with the features, an image to load to Facebook, a placeholder for show any error and a footer with our logo, will look like this:





"Dashboard.jade"

#header-container
    header.wrapper.clearfix
        .new
            a#nn(href="#")= t('dashboard.new_nomination')
        .loading
            img(src="/images/loading.png")
        .exit
            a(href="/logout")= t('dashboard.exit')
#main-container
    #main.wrapper.clearfix
        .nominations
            div.nheader.title #{t('dashboard.mine')}
            ul
                li
                    input(type="checkbox", checked="true")
                    label  Nomination 1
                li
                    input(type="checkbox")
                    label  Nomination 2
                li
                    input(type="checkbox")
                    label  Nomination 3
            div.title #{t('dashboard.appear')}
            ul
                li
                    input(type="checkbox")
                    label  Nomination a
                li
                    input(type="checkbox")
                    label  Nomination b
                li
                    input(type="checkbox")
                    label  Nomination d
                li
                    input(type="checkbox")
                    label  Nomination e
                li
                    input(type="checkbox")
                    label  Nomination f
                li
                    input(type="checkbox")
                    label  Nomination g
                li
                    input(type="checkbox")
                    label  Nomination h

            div.title #{t('dashboard.voted')}
            ul
                li
                    input(type="checkbox")
                    label  Nomination i
                li
                    input(type="checkbox")
                    label  Nomination ii
                li
                    input(type="checkbox")
                    label  Nomination iii
        .details
            fieldset.fs
                legend Nomination 1
                .header
                    .users #{t('dashboard.users')}
                    .add
                        a#am(href="#")#{t('dashboard.add_more')}
                    .date Date
                br
                .list
                    table.userst
                        thead
                            tr
                                th #{t('dashboard.name')}
                                th #{t('dashboard.votes')}
                                th
                        tbody
                            tr
                                td User 1
                                td 2
                                td
                                    a(href="#") #{t('dashboard.vote')}
                                    | /
                                    a(href="#") #{t('dashboard.erase')}
                            tr
                                td User 2
                                td 3
                                td
                                    a(href="#") #{t('dashboard.vote')}
                                    | /
                                    a(href="#") #{t('dashboard.erase')}
                            - for (var i =4; i                                tr
                                    td User #{i}
                                    td #{Math.floor(Math.random()*11)}
                                    td
                                        a(href="#") #{t('dashboard.vote')}
                                        | /
                                        a(href="#") #{t('dashboard.erase')}
                .btns
                    button.myButton#end #{t('dashboard.end_nomination')}
                    button.myButton#remove #{t('dashboard.remove_me')}
                    button.myButton#cancel #{t('dashboard.cancel')}
        .images
        .logo logo
        .ficon
            img(src="/images/ipn.png")
#footer-container
    footer.wrapper

#dialog-new(title="Create new nomination")
    p.validateTips All form fields are required
    form
        fieldset
            label.forml(for="name") Name:
            input#name.text.ui-widget-content.ui-corner-all(type="text", name="name")
            br
            label.forml(for="email") End date:
            input#datep.text.ui-widget-content.ui-corner-all(type="text", name="datep")

#dialog-add(title="Add friends")
    form
        fieldset
            ol#selectable
                li.ui-state-default 1
                li.ui-state-default 2
                li.ui-state-default 3
                li.ui-state-default 4
                li.ui-state-default 5
                li.ui-state-default 6


We are adding a lot of things that we are going to erase later, but its to see if this is looking ok, the only interesting thing here will be the "for" to add some users around lines 89-96, we will fill out this from the app later.


We have two lost forms at the end, but we will hide them because we will use them as dialogs with jquery-ui.


Lets update our layout to add our css and the js needed

link(rel="stylesheet", href="/stylesheets/jquery-ui.css", type="text/css")

script(defer, src="/javascripts/lib/jquery-ui.min.js")


Lets start our dialogs and a date picker, we add al this to "script.js"

    $( "#datep" ).datepicker();
    $( "#selectable" ).selectable();
    $("#nn").click(function(){
        $( "#dialog-new" ).dialog( "open" );
    });
    $("#am").click(function(){
        $( "#dialog-add" ).dialog( "open" );
    });
    $( "#dialog-new" ).dialog({
  autoOpen: false,
  height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Create a nomination": function() {
    $( this ).dialog( "close" );
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });
    $( "#dialog-add" ).dialog({
        autoOpen: false,
  height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Add friend(s)": function() {
    $( this ).dialog( "close" );
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });


Then we also one that only one checkbox can be selected at the time and also when they select a label the checkbox get selected ask, we use some query magic and add it also to "script.js"

    function checkOne(){
        var currentEl = $(this);
        $("input:checked").each(function(){
            $(this).attr('checked', false);
        });
        if (currentEl.is('input')){
            currentEl.attr('checked', true);
            return;
        }
        var checkbox = currentEl.siblings('input');
        checkbox.attr('checked', !checkbox.attr('checked'));
    }
    $(".nominations ul label").click(checkOne);
    $(".nominations ul input").click(checkOne);


Here we just listen to the click event in the checkbox and the label, we deselect the already selected and we select the current one

We end up with something like this:



and the dialogs







And the css that we are going to add is:

/* =============================================================================
   Primary styles
   Author: mrpix
   ========================================================================== */
.logo{
    text-align: center;
}
.list{
    margin-top: 20px;
}
.btns{
    margin-top: 5%;
}
#main-container{
    height: 500px;
}
#main{
    height: 100%;
}
.userst th, .userst td{
    border: 1px solid #ededed;
}
.userst th{
    background-color: #ededed
}
.userst{
    width: 95%;
}
.btns button{
    margin-left: 5%;
}
.users, .add{
    float: left;
    margin-right: 30%;
}
.date{
    float: right;
}
.nominations .title{
    background-color: #ededed;
    width: 60%;
    padding: 0 0 0 40px;
    border: 1px solid black;
    text-align: left;
}
.nominations ul{
    border: 1px solid black;
    width: 60%;
    height: 120px;
    overflow: auto;
    margin-bottom: 0;
}
.nheader{
    -webkit-border-top-left-radius: 13px;
    -webkit-border-top-right-radius: 13px;
    -moz-border-radius-topleft: 13px;
    -moz-border-radius-topright: 13px;
    border-top-left-radius: 13px;
    border-top-right-radius: 13px;
}
.nominations li{
    list-style:none;
    background-color: white;
    margin-left: 0;
    border-bottom: 1px solid #ededed;
    margin-left: -25px;
}
label{
    padding-left: 5px;
    padding-right: 5px;
}
.nominations{
    width: 30%;
    height: 90%;
    overflow: auto;
    float: left;
}
.details{
    width: 69%;
    height: 90%;
    overflow: auto;
    float: left;
}
.new{
    float: left;
}
.loading{
    text-align: center;
}
.exit{
    float: right;
    margin-top: -1.7em;
}

.copy {
    text-align: center;
}
.ficon{
 float: right;
}

.title{
 text-align: center;
}
.att_table
{

 background: #fff;
 margin: 0px 40px 3px 50px;
 width: 90%;
 border-collapse: collapse;
 text-align: left;
}
.att_table th
{

 color: #039;
 padding: 10px 8px;
 border-bottom: 2px solid #6678b1;
}
.att_table td
{
 border-bottom: 1px solid #ccc;
 color: #669;
 padding: 6px 8px;
}
.att_table tbody tr:hover td
{
 color: #00c;
}
.myButton {
    -moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
 -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
 box-shadow:inset 0px 1px 0px 0px #ffffff;
 background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) );
 background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% );
 filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#dfdfdf');
 background-color:#ededed;
 -moz-border-radius:6px;
 -webkit-border-radius:6px;
 border-radius:6px;
 border:1px solid #dcdcdc;
 display:inline-block;
 color:#777777;
 font-family:arial;
 font-size:15px;
 font-weight:bold;
 padding:6px 24px;
 text-decoration:none;
 text-shadow:1px 1px 0px #ffffff;
}.myButton:hover {
 background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #dfdfdf), color-stop(1, #ededed) );
 background:-moz-linear-gradient( center top, #dfdfdf 5%, #ededed 100% );
 filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dfdfdf', endColorstr='#ededed');
 background-color:#dfdfdf;
}.myButton:active {
 position:relative;
 top:1px;
}
#dialog-add, #dialog-new{
    display: none;
}
#selectable .ui-selecting { background: #FECA40; }
#selectable .ui-selected { background: #F39814; color: white; }
#selectable { list-style-type: none; margin: 0; padding: 0; }
#selectable li { margin: 3px; padding: 1px; float: left; width: 50px; height: 40px; font-size: 2em; text-align: center; }


If you need help understanding the css let me know in the comments

The design is not the best but we are making progress, any suggestions is welcome


Fork the code in github:


Greetings