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\$ – Dan Aug 10 '15 at 9:51