We've been hard at work creating a browser-based GUI/Client for playing the Cardshifter TCG for the past week or so. Today, I just finished doing a pretty complicated layout for the chat lobby, along with @SirPython who put together the JavaScript/AngularJS code, which I will include with his permission.
Here's the full repo on GitHub, in case you're interested.
I have not worked with table-based HTML/CSS layouts for a long time, and I wanted to know if the code was well-structured, if I'm using CSS and Bootstrap efficiently, etc.
Here is lobby.html. Note that the sections of text {{in brackets}} are injected by Angular. The header and other things are in a separate "mother" HTML file, where this child page (and others) get injected with the flow of the game.
<table id="lobby"> <tr id="lobby-headers"> <td id="lobby-title">Cardshifter Lobby</td> <td id="lobby-deck-builder" width="20%"><button class="btn btn-navbar csh-button">Deck Builder</button></td> </tr> <tr id="lobby-invite-request" ng-show="gotInvite"> <td colspan="2"> <div id="lobby-accept-invite"> Game invite from {{invite.name}} to play {{invite.type}}!<br/> <input ng-click="acceptInvite(true)" type="button" value="Accept" class="btn btn-success"/> <input ng-click="acceptInvite(false)" type="button" value="Decline" class="btn btn-danger"/> </div> </td> </tr> <tr id="lobby-list-headers"> <th id="lobby-message-list-header">Messages</th> <th id="lobby-users-list-header">Users Online</th> </tr> <tr id="lobby-lists" height="400px"> <td id="lobby-message-list"> <ul id="lobby-chat-messages"> <li ng-repeat="message in chatMessages" id="lobby-chat-message"> [{{message.timestamp}}] {{message.from}}: {{message.message}} </li> </ul> </td> <td id="lobby-users-list"> <ul id="lobby-users"> <li ng-repeat="user in users" id="lobby-user"> <input ng-model="$parent.$parent.selected_opponent" ng-if="user.userId!=currentUser.id" type="radio" value="{{$parent.user.userId}}" name="user_selection" /> {{user.name}} </li> </ul> </td> </tr> <tr> <td id="lobby-message"> <textarea ng-model="user_chat_message" ng-keyup="sendMessage($event)" id="lobby-chat-text-area" rows="1" cols="75" wrap="off" placeholder="Enter chat message..."></textarea> <input ng-click="sendMessage()" ng-disabled="sending" type="submit" value="Send" class="btn btn-navbar csh-button"/> </td> <td id="lobby-invite"> <input ng-click="startGame()" type="button" value="Invite to game" class="btn btn-warning"/> </td> </tr> <tr id="lobby-mods"> <td colspan="2" id="lobby-mod-selection"> <form class="form-inline" role="form"> <div class="form-group"> <label for="mod_selection">Select game type:</label> <div ng-repeat="mod in mods" class="form-control" id="lobby-mod-selector"> <input ng-model="$parent.selected_mod" type="radio" value="{{mod}}" name="mod_selection" id="mod_selection"/> {{mod}} </div> </div> </form> </td> </tr> </table> Here is lobby.css which is linked to it. Note there are a number of empty selector classes, which all exist but some are not being used at the moment, and may be in the future if needed.
/* WHOLE LOBBY */ #lobby { width: 100%; } /* TABLE HEADERS */ #lobby-headers { font-family: Georgia, Times, "Times New Roman", serif; text-align: center; color: #DDDDDD; background-color: #000000; } #lobby-title { font-size: 1.5em; font-weight: bold; } #lobby-deck-builder {} /* SECTION HEADERS */ #lobby-list-headers { font-family: Georgia, Times, "Times New Roman", serif; font-size: 1.2em; } #lobby-message-list-header { text-align: center; } #lobby-users-list-header { text-align: center; } /* MAIN MESSAGE & USERS SECTIONS */ #lobby-lists { vertical-align: text-top; } #lobby-message-list { font-size: 0.8em; !important } /* List of all messages */ #lobby-chat-messages { list-style-type: none; padding-left: 0; } /* Each individual message line */ #lobby-chat-message { } #lobby-users-list { font-size: 0.9em; font-family: Georgia, Times, "Times New Roman", serif; } /* List of all users */ #lobby-users { list-style-type: none; padding-left: 0; } /* Each individual user line */ #lobby-user { } /* FOOTER SECTIONS */ #lobby-message { background-color: #000000; vertical-align: bottom; } /* TEXT AREA FOR TYPING CHAT MESSAGES*/ #lobby-chat-text-area { outline: none; overflow: auto; vertical-align: middle; } #lobby-invite { background-color: #000000; text-align: center; } #lobby-mods {} #lobby-mod-selection {} /* DIV CONTAINING RADIO BUTTON AND MOD NAME */ #lobby-mod-selector { border: 1; } /* Game invite accept dialog */ #lobby-invite-request { font-family: Georgia, Times, "Times New Roman", serif; font-size: 1.6em; text-align: center; background-color: #0033CC; color: #EEEEEE; border-top-color: #FFFFFF; vertical-align: middle; } Here is the lobby_controller.js, it is AngularJS written by @SirPython, which links the client to the server messages:
CardshifterApp.controller("LobbyController", function($scope, $timeout) { var CHAT_FEED_LIMIT = 10; var ENTER_KEY = 13; var MESSAGE_DELAY = 3000; var ENTER_KEY = 13; $scope.users = []; $scope.chatMessages = []; $scope.mods = []; $scope.currentUser = window.currentUser; $scope.invite = { id: null, name: null, type: null }; $scope.gotInvite = false; var commandMap = { "userstatus": updateUserList, "chat": addChatMessage, "inviteRequest": displayInvite, "availableMods": displayMods, "newgame": enterNewGame }; var getUsers = new CardshifterServerAPI.messageTypes.ServerQueryMessage("USERS", ""); CardshifterServerAPI.sendMessage(getUsers); CardshifterServerAPI.setMessageListener(function(message) { commandMap[message.command](message); $scope.$apply(); // needs to manually updated since this is an event }, ["userstatus", "chat", "inviteRequest", "availableMods", "newgame"]); $scope.sendMessage = function(e) { if(e && e.keyCode !== ENTER_KEY) { // user may hit "enter" key return; } $scope.sending = true; var chatMessage = new CardshifterServerAPI.messageTypes.ChatMessage($scope.user_chat_message); CardshifterServerAPI.sendMessage(chatMessage); $scope.user_chat_message = ""; // clear the input box $timeout(function() { // allow another message to be sent in 3 seconds $scope.sending = false; }, MESSAGE_DELAY); } $scope.startGame = function() { if($scope.selected_mod && $scope.selected_opponent) { var startGame = new CardshifterServerAPI.messageTypes.StartGameRequest($scope.selected_opponent, $scope.selected_mod); CardshifterServerAPI.sendMessage(startGame); } else { // user needs to choose an opponent and/or a mod console.log("need to choose mod and/or opponent"); } } $scope.acceptInvite = function(accept) { var accept = new CardshifterServerAPI.messageTypes.InviteResponse($scope.invite.id, accept); CardshifterServerAPI.sendMessage(accept); $scope.gotInvite = false; } // The command map functions: /** * Based on the content of message, will add or remove * a user from the user list. */ function updateUserList(message) { if(message.status === "OFFLINE") { for(var i = 0, length = $scope.users.length; i < length; i++) { if($scope.users[i].userId === message.userId) { $scope.users.splice(i, 1); // remove that user from the array break; } } } else { $scope.users.push(message); } } /** * Adds a chat message to the message feed. If the message * feed is at the maximum limit of messages, deletes the oldest * message. */ function addChatMessage(message) { if($scope.chatMessages.length === CHAT_FEED_LIMIT) { // remove the oldest chat message $scope.chatMessages.shift(); } var now = new Date(); var YMD = now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDate(); var HMS = now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(); message.timestamp = YMD + " " + HMS; $scope.chatMessages.push(message); } /** * Shows buttons and a message to this client for accepting * or declining a game request. */ function displayInvite(message) { $scope.invite.id = message.id; $scope.invite.name = message.name; $scope.invite.type = message.gameType; $scope.gotInvite = true; } /** * Shows to the user a list of all available mods. */ function displayMods(message) { $scope.mods = message.mods; } /** * Stores the game ID in currentUser for other controllers * to use and navigates to the deck-builder page for the * user to select a deck. */ function enterNewGame(message) { currentUser.currentGameId = message.gameId; console.log("change to game"); } }); 

id="lobby-user"on an ng-repeat. This is invalid; you cannot have more than one HTML element with the same ID. Use a class! \$\endgroup\$