The client app - HTML5, CSS3 and Bootstrap
Contents
Here we have our basic index.html file which already has a few advanced features built in:
- Support for portrait and landscape mode (see also CSS media queries)
- Responsive design by using Bootstrap
- Three column design for the team, summary of current user story, list of user stories in the current sprint
Lets quickly walk through the major building blocks of the code below.
Handlebars:
In lines 16 to 46 you’ll find a few Handlebars templates for filling the three columns “Team”, “Summary”, “Stories”. See the corresponding script tag in line 49. Pay particular attention to line 37, where a comparison helper function ‘equal’ is used. This helper function is registered in our code on line 555 to 587.
Drawer:
We use the drawer feature of Bootstrap to slide in a panel which includes the main menu entries “Login”, “Messages”, “Logout”. The drawer is defined on line 64 to 82.
Main UI:
Line 89 to 186 contains the main UI elements. Title, menu button, story point buttons, and three tab panes for the team member’s online status, and the stories in the current sprint.
Dialogs:
Line 187 until EOF contain the various dialogs and message boxes which appear during the poker session:
- Error message
- Log messages
- Login dialog
- Logout dialog
- Switch user story (admin only)
- Story summary (only visible in portrait mode when clicking on a user story in the right panel)
- Processing… message
<!DOCTYPE html> <html lang="en" manifest="app.manifest"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>CSC Planning Poker</title> <!-- build:css css/style.css --> <link href="../bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="../bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"> <link href="../bower_components/bootstrap-drawer/dist/css/bootstrap-drawer.min.css" rel="stylesheet"> <link href="css/style.css" rel="stylesheet"> <!-- endbuild --> <!-- Handlebars templates --> <script id="hbs_users" type="text/x-handlebars-template"> <div class="ppPnl"> <span class="ppName">{{firstname}} {{name}}</span> <span class="ppPillcnt"> <img class="ppPill" src="assets/pill_{{color}}_sm.png"/> <span class="ppPillLbl">{{#equal role "admin"}} {{tmpPoints}} {{else}} {{points}} {{/equal}}</span> </span> </div> </script> <script id="hbs_summary" type="text/x-handlebars-template"> <div class="ppPnl row_summary"> <span class="ppHeadline">Story {{seqno}} ({{id}})</span> <p>{{title}}</p> <p>{{summary}}</p> <p class="ppHeadline">Prerequisites:</p> <p>{{prerequisites}}</p> <p class="ppHeadline">Acceptance Criteria:</p> <p>{{acceptance_criteria}}</p> </div> </script> <script id="hbs_stories" type="text/x-handlebars-template"> <a href="{{#equal role 'admin'}}#ppModalSwitchStory{{else}}#ppModalSummary{{/equal}}" data-toggle="modal"> <div class="ppPnl ppStory" data-row="{{seqno}}"> <span class="ppName">Story {{seqno}} ({{id}})</span> <span id="" class="ppPillcnt"> <img class="ppPill" src="assets/pill_{{color}}_sm.png"/> <span class="ppalle">{{sumpoints}}</span> </span> </div> </a> </script> <!-- build:js js/lib.js --> <script src="../bower_components/jquery/dist/jquery.min.js"></script> <script src="../bower_components/handlebars/handlebars.min.js"></script> <script src="../bower_components/bootstrap/dist/js/bootstrap.min.js"></script> <script src="../bower_components/bootstrap-drawer/dist/js/drawer.min.js"></script> <script src="../bower_components/rxjs/dist/rx.all.min.js"></script> <script src="../bower_components/rxjs-dom/dist/rx.dom.js"></script> <script src="js/namespace.js"></script> <script src="js/app.js"></script> <!-- endbuild --> </head> <body class="has-drawer"> <div id="ppDrawer" class="drawer drawer-right dw-xs-5 dw-sm-3 dw-md-2 fold" aria-labelledby="drawerExample"> <!--<div class="drawer-controls">--> <!--<a href="#drawerExample" data-toggle="drawer" aria-foldedopen="false" aria-controls="drawerExample"--> <!--class="btn btn-primary btn-sm">Menu</a>--> <!--</div>--> <div class="drawer-contents"> <div class="ppDrawerHead"> <span class="ppLft">Menu</span> <a href="#ppDrawer" data-toggle="drawer" aria-controls="ppDrawer" class="ppRgt"> <span class="glyphicon glyphicon-menu-hamburger" aria-hidden="true"></span></a> </div> <div class="drawer-heading"> </div> <div class="drawer-body"> </div> <ul class="drawer-nav"> <li role="presentation" class="active"><a href="#ppModalLogin" data-toggle="modal">Login</a></li> <li role="presentation"><a href="#ppModalMessages" data-toggle="modal">Messages</a></li> <li role="presentation"><a href="#ppModalLogout" data-toggle="modal">Logout</a></li> </ul> <div class="drawer-footer"> </div> </div> </div> <!--Container - This is the main container of the app. Components included in here are:--> <!-- - Menu Icon (drawer)--> <!-- - Title (Planning Poker)--> <!-- - Admin controls (Flip Cards, Reset Votes --> <!-- - Scrum poker cards (buttons)--> <!-- - Panels for participants, story text, average points--> <div class="container"> <header> <div class="ppLft"> <span>Planning Poker</span><br> <span id="ppFullName"></span> </div> <div class="ppRgt"> <a href="#ppDrawer" data-toggle="drawer" aria-controls="ppDrawer"> <span class="glyphicon glyphicon-menu-hamburger" aria-hidden="true"></span> </a> </div> </header> <main> <div id="ppAdmin" class="ppHide"> <div class="col-xs-3 col-md-2"> <a id="ppFlipCardsBtn" role="button" class="btn btn-primary btn-block ppAdminBtn">Flip Cards</a> </div> <div class="col-xs-3 col-md-2"> <a id="ppResetVotesBtn" role="button" class="btn btn-primary btn-block ppAdminBtn">Reset Votes</a> </div> </div> <div class="btn-group btn-group-justified" role="group" aria-label="..."> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">0</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">1/2</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">1</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">2</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">3</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">5</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">8</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">13</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">20</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">40</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">100</a> </div> <div class="col-xs-2 col-md-1"> <a role="button" class="btn btn-primary btn-block ppBtn">?</a> </div> </div> <!-- Tab panes (only visible in portrait mode)--> <div id="panels" class="ppPrt"> <!-- Nav tabs --> <ul class="nav nav-tabs tab" role="tablist" data-tabs="tabs"> <li role="presentation" class="active"> <a href="#panel_members" aria-controls="member" role="tab" data-toggle="tab">Members</a> </li> <li role="presentation"> <a href="#panel_summary" aria-controls="summary" role="tab" data-toggle="tab">Summary</a> </li> <li role="presentation"> <a href="#panel_stories" aria-controls="stories" role="tab" data-toggle="tab">Stories</a> </li> </ul> <!-- Tab panes --> <div class="tab-content"> <div id="panel_members" role="tabpanel" class="tab-pane active members"><p></p></div> <div id="panel_summary" role="tabpanel" class="tab-pane summary"><p></p></div> <div id="panel_stories" role="tabpanel" class="tab-pane stories"><p></p></div> </div> </div> <!-- Three columns for participants, story summary, and story point (only visible in landscape mode) --> <div id="columns" class="row ppLnd"> <div class="col-md-3 members"> <p class="panel_header">Team</p> </div> <div class="col-md-6 summary"> <p class="panel_header">Summary</p> </div> <div class="col-md-3 stories"> <p class="panel_header">Stories</p> </div> </div> </main> </div> <!-- Error Message--> <div id="ppModalError" class="modal fade" role="dialog"> <div class="modal-dialog"> <!-- Modal content--> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">×</button> <h4 class="modal-title">Error</h4> </div> <div id="ppErrorBody" class="modal-body"> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> </div> </div> </div> <!-- Log Messages --> <div id="ppModalMessages" class="modal fade" role="dialog"> <div class="modal-dialog"> <!-- Modal content--> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">×</button> <h4 class="modal-title">Messages</h4> </div> <div id="ppMessagesBody" class="modal-body"> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> </div> </div> </div> <!-- Modal Login--> <div class="modal fade" id="ppModalLogin" role="dialog"> <div class="modal-dialog modal-sm"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">×</button> <h4 class="modal-title">Login</h4> </div> <div class="modal-body"> <input id="ppLogin" placeholder="username" autofocus type="text" pattern="[A-Za-z0-9]"/><br><br> <input id="ppPassword" placeholder="password" type="password"/> </div> <div class="modal-footer"> <button id="ppLoginBtn" type="submit" class="btn btn-default" data-dismiss="modal">OK</button> </div> </div> </div> </div> <!-- Modal Logout--> <div class="modal fade" id="ppModalLogout" role="dialog"> <div class="modal-dialog modal-sm"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">×</button> <h4 class="modal-title">Logout</h4> </div> <div class="modal-body">Do you want to logout ? </div> <div class="modal-footer"> <button id="ppLogoutBtnOk" type="button" class="btn btn-default" data-dismiss="modal">OK</button> <button id="ppLogoutBtnCancel" type="button" class="btn btn-default" data-dismiss="modal">Cancel </button> </div> </div> </div> </div> <!-- Modal Switch Stories--> <div class="modal fade" id="ppModalSwitchStory" role="dialog"> <div class="modal-dialog modal-sm"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">×</button> <h4 class="modal-title">Switch Story</h4> </div> <div class="modal-body"><p id="switchStoryModalBody">Do you want to switch to story"></p></div> <div class="modal-footer"> <button id="ppSwitchBtnOk" type="button" class="btn btn-default" data-dismiss="modal">Yes</button> <button id="ppSwitchBtnCancel" type="button" class="btn btn-default" data-dismiss="modal">No </button> </div> </div> </div> </div> <!-- Modal Summary--> <div class="modal fade" id="ppModalSummary" role="dialog"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal">×</button> <h4 class="modal-title">Summary</h4> </div> <div class="modal-body"><p></p> </div> <div class="modal-footer"> <button id="ppSummaryBtnOk" type="button" class="btn btn-default" data-dismiss="modal">OK</button> </div> </div> </div> </div> <!-- "Processing" waiting message --> <div class="modal fade" id="ppWaitDialog" role="dialog" aria-hidden="true" data-backdrop="static" data-keyboard="false"> <div class="modal-dialog modal-sm"> <div class="modal-content"> <div class="modal-header"> <h3>Processing...</h3> </div> <div class="modal-body"> <div class="progress"> <div class="progress-bar progress-bar-info progress-bar-striped active" style="width: 100%"> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> </div> </div> </div> </body> </html>
For our Javascript code, we create the namespace app in order to avoid name conflicts with other libraries.
var app = app || {}; app.model = app.model || {}; app.CONST = app.CONST || {}; app.CONST.ppBtnDefCol = "#337AB7"; app.CONST.ppWhiteCol = "#F0F0F0"; app.CONST.defPillColor = "grey"; app.STATUS = app.STATUS || {}; app.STATUS.userNo = ""; app.STATUS.user = ""; app.STATUS.role = ""; app.STATUS.curStory = null; app.STATUS.stories = null; app.TEMPLATES = app.TEMPLATE || {}; app.TEMPLATES.members = Handlebars.compile($("#hbs_users").html());
app.js
Let’s go quickly through the main UI building blocks of the code. Line 10 to 15 opens the drawer
if the user clicks on the menu button in the upper right corner. Line 17 to 28 containes the logic
behind the Login button and effectively trying to establish a WebSocket connection with the server.
Line 19 to 37 handles the case when the user presses Enter either in the Login or Password entry fields of
the Login dialog.
'use strict'; $(document).ready(function () { app.CONST.SERVER_URL = ("ws://localhost:8080/poker/clientSocket/"); var socket; // Make sure the three content columns reach down to the bottom of the browser window resizeElementHeight(document.querySelector('.tab-content')); // Initialize the drawer and set up auto close $('body').click(function () { $('#ppDrawer').drawer('hide'); // When you return false from an event handler, it prevents the default action // for that event and stops the event bubbling up through the DOM. return true; }); $('#ppLoginBtn').click(function () { /* The jQuery .val() method is primarily used to get the values of form elements such as input, select and textarea. */ app.STATUS.user = $('#ppLogin').val(); if (app.STATUS.user) { if (socket) socket.onCompleted(); /* Upon each connect(), all data is newly sent by the server, thus effectively resetting the clients UI and user/admin logic. */ socket = connect(); } }); /* When typing into the login id input field, this callback will be triggered by the enter key and fires the OK button click event. Without this, the enter key wouldn't have any effect in the input field. */ $("#ppLogin, #ppPassword").on("keyup", function (event) { if (event.keyCode === 13) { $("#ppLoginBtn").click(); } }); // Disconnect from socket and clear UI if user logs out. $('#ppLogoutBtnOk').click(function () { if (socket) { socket.onCompleted(); disconnect(); } }); // Switch Story $('#ppSwitchBtnOk').click(function () { if (app.STATUS.role === "admin") { socket.onNext("story " + app.STATUS.tmpCurStory); } }); $('#ppResetVotesBtn').click(function () { socket.onNext("reset votes"); }); $('#ppFlipCardsBtn').click(function () { if (app.STATUS.curStory) socket.onNext("flip cards"); }); /* Using RxJS, we create an observable that generates a stream of click events from all buttons of class ".ppBtn". If a button is clicked, the background of all buttons is reset and then the button that was clicked gets a green background. */ Rx.Observable.fromEvent($(".ppBtn"), 'click') .filter(function () { // For the buttons to work, the user must have logged in and the admin selected a story return socket && (app.STATUS.curStory !== null); }) .map(function (e) { $(".ppBtn").css("background-color", app.CONST.ppBtnDefCol); $(e.target).css("background-color", "green"); sendPoints($(e.target).text()); }) .subscribe();