summaryrefslogtreecommitdiff
path: root/WebInterface/src
diff options
context:
space:
mode:
authorDennis Kobert <d-kobert@web.de>2019-06-11 23:53:30 +0200
committerDennis Kobert <d-kobert@web.de>2019-06-11 23:53:30 +0200
commit3a3d0fc3d4733f8908e23a03f860d76340479ec4 (patch)
treecf4b82f61d01d2a24836e9820d73972436847982 /WebInterface/src
parentc28c9fafa2c74b101f7ce777aac722dcdeecefc6 (diff)
Reorganize Project structure
Diffstat (limited to 'WebInterface/src')
-rw-r--r--WebInterface/src/js/about.js6
-rw-r--r--WebInterface/src/js/index.js12
-rw-r--r--WebInterface/src/js/modules/interface.js82
-rw-r--r--WebInterface/src/js/modules/networking/commands/_command.js28
-rw-r--r--WebInterface/src/js/modules/networking/commands/login/createServer.js23
-rw-r--r--WebInterface/src/js/modules/networking/commands/login/listServers.js43
-rw-r--r--WebInterface/src/js/modules/networking/commands/login/login.js54
-rw-r--r--WebInterface/src/js/modules/networking/commands/loginCmds.js36
-rw-r--r--WebInterface/src/js/modules/networking/commands/playCmds.js31
-rw-r--r--WebInterface/src/js/modules/networking/hash.js20
-rw-r--r--WebInterface/src/js/modules/networking/networker.js89
-rw-r--r--WebInterface/src/js/modules/ui/collections/about.js14
-rw-r--r--WebInterface/src/js/modules/ui/collections/login.js24
-rw-r--r--WebInterface/src/js/modules/ui/collections/play.js19
-rw-r--r--WebInterface/src/js/modules/ui/components/backdrop.js65
-rw-r--r--WebInterface/src/js/modules/ui/components/modal/login-modal.js139
-rw-r--r--WebInterface/src/js/modules/ui/components/modal/modal.js66
-rw-r--r--WebInterface/src/js/modules/ui/components/notification-banner.js133
-rw-r--r--WebInterface/src/js/modules/ui/components/router.js44
-rw-r--r--WebInterface/src/js/modules/ui/components/server-listing.js91
-rw-r--r--WebInterface/src/js/modules/ui/uiManager.js60
-rw-r--r--WebInterface/src/js/play.js12
-rw-r--r--WebInterface/src/js/src_old/modules/playModule.js46
-rw-r--r--WebInterface/src/js/src_old/modules/server-client.js93
-rw-r--r--WebInterface/src/js/src_old/modules/ui/login-modal.js181
-rw-r--r--WebInterface/src/js/src_old/modules/ui/modal.js67
-rw-r--r--WebInterface/src/js/src_old/modules/ui/server-listing.js67
-rw-r--r--WebInterface/src/style/about.scss43
-rw-r--r--WebInterface/src/style/index.scss10
-rw-r--r--WebInterface/src/style/partials/_base.scss20
-rw-r--r--WebInterface/src/style/partials/_btn.scss75
-rw-r--r--WebInterface/src/style/partials/_colors.scss18
-rw-r--r--WebInterface/src/style/partials/_game.scss13
-rw-r--r--WebInterface/src/style/partials/backdrop/_base.scss58
-rw-r--r--WebInterface/src/style/partials/backdrop/_menu.scss43
-rw-r--r--WebInterface/src/style/partials/front-layer/_base.scss23
-rw-r--r--WebInterface/src/style/partials/front-layer/_copyright.scss10
-rw-r--r--WebInterface/src/style/partials/front-layer/_notifications.scss84
-rw-r--r--WebInterface/src/style/partials/front-layer/_server-listing.scss121
-rw-r--r--WebInterface/src/style/partials/game/_chat.scss28
-rw-r--r--WebInterface/src/style/partials/modal/_base.scss35
-rw-r--r--WebInterface/src/style/partials/modal/_login.scss55
-rw-r--r--WebInterface/src/style/play.scss17
43 files changed, 2198 insertions, 0 deletions
diff --git a/WebInterface/src/js/about.js b/WebInterface/src/js/about.js
new file mode 100644
index 0000000..23351cf
--- /dev/null
+++ b/WebInterface/src/js/about.js
@@ -0,0 +1,6 @@
+import Interface from './modules/interface';
+import UIManager from './modules/ui/uiManager';
+
+let iface = new Interface();
+let uiMan = new UIManager(iface);
+uiMan.initAbout();
diff --git a/WebInterface/src/js/index.js b/WebInterface/src/js/index.js
new file mode 100644
index 0000000..c8b5ca8
--- /dev/null
+++ b/WebInterface/src/js/index.js
@@ -0,0 +1,12 @@
+import Interface from './modules/interface';
+import UIManager from './modules/ui/uiManager';
+import Networker from './modules/networking/networker';
+
+const SERVERURL = 'https://kobert.dev/api/login';
+
+let iface = new Interface();
+let uiMan = new UIManager(iface);
+uiMan.initLogin();
+
+let netMan = new Networker(iface, SERVERURL, true); // TODO: Remove debug flag
+netMan.initLogin();
diff --git a/WebInterface/src/js/modules/interface.js b/WebInterface/src/js/modules/interface.js
new file mode 100644
index 0000000..ac5ea93
--- /dev/null
+++ b/WebInterface/src/js/modules/interface.js
@@ -0,0 +1,82 @@
+/**
+ * Stores an object and it's public methods
+ */
+class InterfaceAccessor {
+ /**
+ * Creates new accessor for object with publicMethods being exposed
+ * @param {object} object
+ * @param {array<String>} publicMethods
+ */
+ constructor(object, publicMethods) {
+ this.object = object;
+ this.publicMethods = publicMethods;
+ }
+
+ /**
+ * Executes method if it is a public method
+ * @param {string} method Name of method to call
+ * @return {number} 0 success, 1 method not public, 2 method not found
+ */
+ execute(method, ...args) {
+ if (!this.publicMethods.includes(method)) return 1;
+ if (typeof this.object[method] != 'function') return 2;
+
+ this.object[method](...args);
+ return 0;
+ }
+}
+
+/**
+ * Implements communication between objects
+ */
+export default class Interface {
+ /**
+ * Initializes interface
+ */
+ constructor() {
+ this.objects = {};
+ }
+
+ /**
+ * Adds a new object to array at objKey and assigns public methods
+ * @param {object} object Object to reference in Interface
+ * @param {String} objKey Key to reference the object under
+ * @param {Array} publicMethods Names of public methods
+ */
+ addObject(object, objKey, publicMethods) {
+ if (!this.objects[objKey]) this.objects[objKey] = [];
+ this.objects[objKey].push(new InterfaceAccessor(object, publicMethods));
+ }
+
+ /**
+ * Unregisters object
+ * @param {Object} object
+ * @param {String} objKey
+ */
+ removeObject(object, objKey) {
+ if (!this.objects[objKey]) return;
+
+ // Remove all instances of object from objKey
+ objects[objKey] = objects[objKey].filter(elt => elt.object != object);
+
+ // Remove reference, if none remain
+ if (objects[objKey].length == 0) objects[objKey] = undefined;
+ }
+
+ /**
+ * Calls a method on all objects with the key objKey
+ * @param {String} objKey Object Key of objects to call method on
+ * @param {String} method Method name to call on the objects
+ * @param {...*} args Arguments to pass
+ * @return {number} 0 Success, 1 no objects with objKey, 2 method not public
+ */
+ callMethod(objKey, method, ...args) {
+ if (!this.objects[objKey]) return 1;
+
+ let returnCode = 0;
+ for (let obj of this.objects[objKey]) {
+ if (obj.execute(method, ...args) != 0) returnCode = 2;
+ }
+ return returnCode;
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/commands/_command.js b/WebInterface/src/js/modules/networking/commands/_command.js
new file mode 100644
index 0000000..46a1a14
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/commands/_command.js
@@ -0,0 +1,28 @@
+/**
+ * Parent Command class which all commands inherit from
+ */
+export default class Command {
+ /**
+ * Constructs basic command object
+ * @param {Interface} iface Interface to communicate over
+ */
+ constructor(iface) {
+ this.iface = iface;
+ }
+
+ /**
+ * Registers public command names to interface
+ * @param {String} name Name to register under
+ * @param {...String} commandNames Names of public commands
+ */
+ registerPublic(name, ...commandNames) {
+ this.iface.addObject(this, name, ['destroy'].concat(commandNames));
+ }
+
+ /**
+ * Removes from iface
+ */
+ destroy() {
+ this.iface.removeObject(this);
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/commands/login/createServer.js b/WebInterface/src/js/modules/networking/commands/login/createServer.js
new file mode 100644
index 0000000..78b2a1b
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/commands/login/createServer.js
@@ -0,0 +1,23 @@
+import Command from '../_command';
+
+/**
+ * Handles creation of Servers
+ */
+export default class CreateServer extends Command {
+ /**
+ * Registers interface for communication with other objects
+ * @param {Interface} iface
+ */
+ constructor(iface) {
+ super(iface);
+ this.registerPublic('createServer', 'createServer');
+ this.refreshing = false;
+ }
+
+ /**
+ * TODO:
+ */
+ createServer() {
+
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/commands/login/listServers.js b/WebInterface/src/js/modules/networking/commands/login/listServers.js
new file mode 100644
index 0000000..2c2bc11
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/commands/login/listServers.js
@@ -0,0 +1,43 @@
+import Command from '../_command';
+
+/**
+ * Handles serverList commands
+ */
+export default class ListServers extends Command {
+ /**
+ * Registers interface for communication with other objects
+ * @param {Interface} iface
+ */
+ constructor(iface) {
+ super(iface);
+ this.registerPublic('listServers', 'listServers');
+ this.refreshing = false;
+ }
+
+ /**
+ * Requests server list from the server
+ */
+ listServers() {
+ if (this.refreshing) return; // If already refreshing, no new request
+
+ let listFn = (groups) => {
+ // Populate server listing
+ this.iface.callMethod('serverListing', 'flushElements');
+ this.iface.callMethod('serverListing', 'addElements', groups, this.iface);
+ // Unbind network function
+ this.iface.callMethod('networker', 'removeHandler', 'ListGroups');
+ this.refreshing = false;
+ };
+ let errorHandler = (err) => {
+ this.refreshing = false;
+ console.error(err.toString());
+ };
+
+ this.iface.callMethod('networker', 'registerHandler',
+ 'ListGroups', listFn);
+ this.iface.callMethod('networker', 'sendRequest',
+ 'GetGroups', errorHandler);
+
+ this.refreshing = true;
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/commands/login/login.js b/WebInterface/src/js/modules/networking/commands/login/login.js
new file mode 100644
index 0000000..44a6c94
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/commands/login/login.js
@@ -0,0 +1,54 @@
+import Command from '../_command';
+import LoginModal from '../../../ui/components/modal/login-modal';
+
+/**
+ * Handles login to server
+ */
+export default class Login extends Command {
+ /**
+ * Registers interface for communication with other objects
+ * @param {Interface} iface
+ */
+ constructor(iface) {
+ super(iface);
+ this.registerPublic('login', 'sendLogin', 'showLogin');
+ this.refreshing = false;
+ }
+
+ /**
+ * Shows a login modal
+ * @param {String} name
+ */
+ showLogin(name) {
+ new LoginModal(this.iface, name);
+ }
+
+ /**
+ * Registers login response method
+ */
+ registerLoginResponse() {
+ this.iface.callMethod('networker', 'registerHandler', 'LoginResponse',
+ (result) => {
+ if (result == 0) {
+ this.iface.callMethod('modal', 'close');
+ this.iface.callMethod('router', 'routePlay');
+ this.iface.callMethod('networker', 'removeHandler',
+ 'LoginResponse');
+ } else {
+ this.iface.callMethod('modal', 'loginFailed', result);
+ }
+ });
+ }
+
+ /**
+ * Sends a login request
+ * @param {string} group Group name to join
+ * @param {string} password Password to send as SHA-256 Base64 String
+ * @param {string} username Display name to use
+ */
+ sendLogin(group, password, username) {
+ this.registerLoginResponse();
+ this.iface.callMethod('networker', 'sendRequest', 'Login',
+ (err) => console.error(err), group, username, password);
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/commands/loginCmds.js b/WebInterface/src/js/modules/networking/commands/loginCmds.js
new file mode 100644
index 0000000..bc5d8a7
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/commands/loginCmds.js
@@ -0,0 +1,36 @@
+import ListServers from './login/listServers';
+import CreateServer from './login/createServer';
+import Login from './login/login';
+
+/**
+ * Manages commands related to the login page
+ */
+export default class LoginCommands {
+ /**
+ * Initializes the login commands
+ * @param {Interface} iface Interface for inter-object communication
+ */
+ constructor(iface) {
+ this.iface = iface;
+ this.cmds = [];
+ this.registerCommands();
+ }
+
+ /**
+ * Registers all the available commands
+ */
+ registerCommands() {
+ this.cmds.push(new ListServers(this.iface));
+ this.cmds.push(new CreateServer(this.iface));
+ this.cmds.push(new Login(this.iface));
+ }
+
+ /**
+ * Destroys all attached commands
+ */
+ destroy() {
+ for (let cmd of this.cmds) {
+ cmd.destroy();
+ }
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/commands/playCmds.js b/WebInterface/src/js/modules/networking/commands/playCmds.js
new file mode 100644
index 0000000..94cd6ba
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/commands/playCmds.js
@@ -0,0 +1,31 @@
+// import ListServers from './login/listServers';
+
+/**
+ * Manages commands related to the login page
+ */
+export default class LoginCommands {
+ /**
+ * Initializes the login commands
+ * @param {Interface} iface Interface for inter-object communication
+ */
+ constructor(iface) {
+ this.iface = iface;
+ this.cmds = [];
+ }
+
+ /**
+ * Registers all the available commands
+ */
+ registerCommands() {
+ // this.cmds.push(new ListServers(iface));
+ }
+
+ /**
+ * Destroys all attached commands
+ */
+ destroy() {
+ for (let cmd of this.cmds) {
+ cmd.destroy();
+ }
+ }
+}
diff --git a/WebInterface/src/js/modules/networking/hash.js b/WebInterface/src/js/modules/networking/hash.js
new file mode 100644
index 0000000..3abcc21
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/hash.js
@@ -0,0 +1,20 @@
+/**
+ * Creates Base64 String with SHA-256 Hash of given string
+ */
+String.prototype.getHash = async function() {
+ let data = new ArrayBuffer(this.length * 2);
+ let bufferView = new Uint16Array(data);
+ for (let i = 0; i < this.length; i++) {
+ bufferView[i] = this.charCodeAt(i);
+ }
+
+ let encrypted = await crypto.subtle.digest('SHA-256', bufferView);
+ let byteArray = new Uint8Array(encrypted);
+ let base64String = '';
+
+ for (let byte of byteArray) {
+ base64String += String.fromCharCode(byte);
+ }
+
+ return btoa(base64String);
+};
diff --git a/WebInterface/src/js/modules/networking/networker.js b/WebInterface/src/js/modules/networking/networker.js
new file mode 100644
index 0000000..199adc7
--- /dev/null
+++ b/WebInterface/src/js/modules/networking/networker.js
@@ -0,0 +1,89 @@
+import * as signalR from '@aspnet/signalr';
+import LoginCommands from './commands/loginCmds';
+import PlayCommands from './commands/playCmds';
+
+/**
+ * Class for communication to server
+ */
+export default class Networker {
+ /**
+ * Creates new Networker and connects it to the Interface
+ * @param {Interface} iface Interface for communication between objects
+ * @param {String} url URL of the server backend
+ * @param {Boolean} [debug=false] Should there be debug output
+ */
+ constructor(iface, url, debug = false) {
+ this.url = url;
+
+ // Register in Interface
+ iface.addObject(this, 'networker',
+ ['sendRequest', 'registerHandler', 'removeHandler']);
+ this.iface = iface;
+
+ const connectionBuilder = new signalR.HubConnectionBuilder()
+ .withUrl(url);
+
+ if (debug) {
+ connectionBuilder.configureLogging(signalR.LogLevel.Trace);
+ } else {
+ connectionBuilder.configureLogging(signalR.LogLevel.Error);
+ }
+
+ this.connection = connectionBuilder.build();
+ this.connection.start()
+ .then(() => this.iface.callMethod('listServers', 'listServers'))
+ .catch((err) => console.error(err.toString()));
+
+ // Initialize refreshing (blocks new refreshes if true)
+ this.refreshing = false;
+ }
+
+ /**
+ * Sending a network request to the server
+ * @param {String} methodName Method to call on server
+ * @param {function} errorHandler Function to call on error
+ * @param {...*} args Arguments to pass to server
+ */
+ sendRequest(methodName, errorHandler, ...args) {
+ this.connection.invoke(methodName, ...args).catch(errorHandler);
+ }
+
+ /**
+ * Register a new function to be called upon receival of message from server
+ * @param {String} name Name of invoked method
+ * @param {function} fn function to call with received data
+ */
+ registerHandler(name, fn) {
+ this.connection.on(name, fn);
+ }
+
+ /**
+ * Removes handler for receiving messages from the server
+ * @param {String} name Name of the invoked method
+ */
+ removeHandler(name) {
+ this.connection.off(name);
+ }
+
+ /**
+ * Initializes Login Commands
+ */
+ initLogin() {
+ this.loginCmd = new LoginCommands(this.iface);
+ }
+
+ /**
+ * Initializes play commands
+ */
+ initPlay() {
+ this.playCmd = new PlayCommands(this.iface);
+ }
+
+ /**
+ * Clears all currently registered commands
+ */
+ clearCommands() {
+ if (this.loginCmd) this.loginCmd.destroy();
+ if (this.playCmd) this.playCmd.destroy();
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/collections/about.js b/WebInterface/src/js/modules/ui/collections/about.js
new file mode 100644
index 0000000..dac8f01
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/collections/about.js
@@ -0,0 +1,14 @@
+import Backdrop from '../components/backdrop';
+
+/**
+ * UI Loader for about page
+ */
+export default class About {
+ /**
+ * Registers components for about page
+ */
+ constructor() {
+ this.backdrop = new Backdrop('menu', 'front-layer', 'show-menu');
+ this.backdrop.initialize();
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/collections/login.js b/WebInterface/src/js/modules/ui/collections/login.js
new file mode 100644
index 0000000..98a6b30
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/collections/login.js
@@ -0,0 +1,24 @@
+import Backdrop from '../components/backdrop';
+import BannerController from '../components/notification-banner';
+import ServerListing from '../components/server-listing';
+
+/**
+ * UI Loader for login page
+ */
+export default class Login {
+ /**
+ * Registers components for login page
+ * @param {Interface} iface Interface to enable comm. with notifications
+ */
+ constructor(iface) {
+ this.backdrop = new Backdrop('menu', 'front-layer', 'show-menu');
+ this.bannerController = new BannerController(iface, 'notifications',
+ 'banner-info', 'dismiss-banner', 'notification-amount');
+ this.serverListing = new ServerListing(iface, 'server-list',
+ 'refresh-button');
+
+ this.backdrop.initialize();
+ this.bannerController.initialize();
+ this.serverListing.initialize();
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/collections/play.js b/WebInterface/src/js/modules/ui/collections/play.js
new file mode 100644
index 0000000..cdea777
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/collections/play.js
@@ -0,0 +1,19 @@
+import Backdrop from '../components/backdrop';
+import BannerController from '../components/notification-banner';
+
+/**
+ * UI Loader for play page
+ */
+export default class Play {
+ /**
+ * Registers components for play page
+ */
+ constructor() {
+ this.backdrop = new Backdrop('menu', 'front-layer', 'show-menu');
+ this.bannerController = new BannerController(iface, 'notifications',
+ 'banner-info', 'dismiss-banner', 'notification-amount');
+
+ this.backdrop.initialize();
+ this.bannerController.initialize();
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/components/backdrop.js b/WebInterface/src/js/modules/ui/components/backdrop.js
new file mode 100644
index 0000000..82ca64f
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/components/backdrop.js
@@ -0,0 +1,65 @@
+/**
+ * Class for adding functionality to backdrop elements
+ */
+export default class Backdrop {
+ /**
+ * Registers all important elements in the backdrop
+ * @param {string} backdropMenu ID of Backdrop Menu
+ * @param {string} frontLayer ID of Front Layer
+ * @param {string} menuButton ID of Show / Hide Menu Button
+ */
+ constructor(backdropMenu, frontLayer, menuButton) {
+ this.ids = {backdropMenu, frontLayer, menuButton};
+ }
+
+ /**
+ * Initializes the components from the ids defined in the constructor
+ */
+ initialize() {
+ this.open = false;
+ this.backdrop = document.getElementById(this.ids.backdropMenu);
+ this.frontLayer = document.getElementById(this.ids.frontLayer);
+ this.menuButton = document.getElementById(this.ids.menuButton);
+
+ this.registerEvents();
+ }
+
+ /**
+ * Registers all neccessary events
+ */
+ registerEvents() {
+ this.registerButtonEvent();
+ this.registerFrontLayerEvent();
+ }
+
+ /**
+ * Registers showing / hiding through menu button
+ */
+ registerButtonEvent() {
+ this.menuButton.addEventListener('click', () => {
+ // Change open state
+ this.open = !this.open;
+
+ // Hide / Unhide Backdrop Menu
+ this.open ? this.backdrop.classList.remove('hidden') :
+ this.backdrop.classList.add('hidden');
+
+ // Set open state for menu button
+ this.open ? this.menuButton.classList.add('open') :
+ this.menuButton.classList.remove('open');
+ });
+ }
+
+ /**
+ * Registers hiding upon front layer interaction
+ */
+ registerFrontLayerEvent() {
+ this.frontLayer.addEventListener('click', () => {
+ if (!this.open) return; // It's already closed
+
+ this.open = false;
+ this.backdrop.classList.add('hidden');
+ this.menuButton.classList.remove('open');
+ });
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/components/modal/login-modal.js b/WebInterface/src/js/modules/ui/components/modal/login-modal.js
new file mode 100644
index 0000000..941fd84
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/components/modal/login-modal.js
@@ -0,0 +1,139 @@
+import Modal from './modal';
+import '../../../networking/hash';
+
+/**
+ * Class to implement a login modal from the parent modal class
+ */
+export default class LoginModal extends Modal {
+ /**
+ * Creates necessary elements for login modal
+ * @param {Interface} iface Interface for Interactions with other Objects
+ * @param {string} serverName Name of the server to connect to
+ */
+ constructor(iface, serverName) {
+ super(serverName);
+ this.serverName = serverName;
+
+ iface.addObject(this, 'modal', ['loginFailed', 'close']);
+ this.iface = iface;
+
+ let passBox = document.createElement('div');
+ let nameBox = document.createElement('div');
+ let sendBox = document.createElement('div');
+
+ let passwordLabel = document.createElement('label');
+ let passwordInput = document.createElement('input');
+ let passwordInvalid = document.createElement('span');
+ passwordLabel.setAttribute('for', 'password-input');
+ passwordLabel.textContent = 'Passwort:';
+ passwordLabel.title = 'Das Passwort des Spiels';
+ passwordInput.id = 'password-input';
+ passwordInput.type = 'password';
+ passwordInput.placeholder = 'Passwort';
+ passwordInput.title = 'Das Passwort des Spiels';
+ passwordInvalid.className = 'invalid hidden';
+ passwordInvalid.textContent = 'Das eingegebene Passwort ist falsch.';
+
+ let nameLabel = document.createElement('label');
+ let nameInput = document.createElement('input');
+ let nameInvalid = document.createElement('span');
+ nameLabel.setAttribute('for', 'name-input');
+ nameLabel.textContent = 'Benutzername:';
+ nameLabel.title = 'Dein Anzeigename';
+ nameInput.id = 'name-input';
+ nameInput.type = 'text';
+ nameInput.autocomplete = 'on';
+ nameInput.placeholder = 'Name';
+ nameInput.title = 'Dein Anzeigename';
+ nameInvalid.className = 'invalid hidden';
+ nameInvalid.textContent =
+ 'Der eingegebene Nutzername ist bereits vergeben.';
+
+ let sendButton = document.createElement('button');
+ sendButton.className = 'btn';
+ sendButton.textContent = 'Login';
+ sendButton.id = 'login-button';
+
+ passBox.appendChild(passwordLabel);
+ passBox.appendChild(passwordInput);
+ passBox.appendChild(passwordInvalid);
+ nameBox.appendChild(nameLabel);
+ nameBox.appendChild(nameInput);
+ nameBox.appendChild(nameInvalid);
+ sendBox.appendChild(sendButton);
+
+ this.body.appendChild(passBox);
+ this.body.appendChild(nameBox);
+ this.body.appendChild(sendBox);
+
+ this.nameInput = nameInput;
+ this.passwordInput = passwordInput;
+ this.loginButton = sendButton;
+
+ this.passwordInvalid = passwordInvalid;
+ this.nameInvalid = nameInvalid;
+
+ this.registerLoginBtnEvent();
+ }
+
+ /**
+ * Method that gets called, if login fails
+ * @param {number} result Error Code
+ */
+ loginFailed(result) {
+ if (result == 1) {
+ this.invalid('Name');
+ this.loginButton.addEventListener('click', this.event);
+ } else if (result == 2) {
+ this.invalid('Pass');
+ this.loginButton.addEventListener('click', this.event);
+ } else {
+ this.iface.callMethod('notifications', 'show', 'failed',
+ 'Ein unbekannter Fehler ist aufgetreten');
+ this.close();
+ }
+ }
+
+ /**
+ * Registers event to send login, on button press
+ */
+ registerLoginBtnEvent() {
+ this.event = () => {
+ this.invalid(); // Remove 'invalid' messages
+ this.loginButton.removeEventListener('click', this.event);
+ this.userName = this.nameInput.value;
+ this.passwordInput.value.getHash()
+ .then((result) => {
+ this.iface.callMethod('login', 'sendLogin', this.serverName,
+ result, this.userName);
+ });
+ };
+ this.loginButton.addEventListener('click', this.event);
+ }
+
+ /**
+ * Displays text under invalid password / username
+ * @param {string} inv Which field to display under (Pass / Name)
+ * Blank inv will hide both
+ */
+ invalid(inv) {
+ this.body.classList.remove('frst-warning');
+ this.body.classList.remove('scnd-warning');
+
+ this.passwordInvalid.classList.add('hidden');
+ this.nameInvalid.classList.add('hidden');
+
+ this.passwordInput.style.borderColor = 'none';
+ this.nameInput.style.borderColor = 'none';
+
+ if (inv == 'Pass') {
+ this.body.classList.add('frst-warning');
+ this.passwordInvalid.classList.remove('hidden');
+ this.passwordInput.style.borderColor = '#ef5350';
+ } else if (inv == 'Name') {
+ this.body.classList.add('scnd-warning');
+ this.nameInvalid.classList.remove('hidden');
+ this.nameInput.style.borderColor = '#ef5350';
+ }
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/components/modal/modal.js b/WebInterface/src/js/modules/ui/components/modal/modal.js
new file mode 100644
index 0000000..c4c5119
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/components/modal/modal.js
@@ -0,0 +1,66 @@
+/**
+ * Parent class to create Modals on the screen
+ * Contains no content, as that is implemented by child classes
+ */
+export default class Modal {
+ /**
+ * Creates a new modal with a title and empty content
+ * @param {string} titleString Title to show at the top of the modal
+ */
+ constructor(titleString) {
+ let modalBackground = document.createElement('div');
+ let modal = document.createElement('div');
+ let title = document.createElement('h1');
+ let body = document.createElement('div');
+
+ modalBackground.className = 'modal-container';
+ modal.className = 'modal';
+ title.className = 'modal-title';
+ body.className = 'modal-body';
+ title.textContent = titleString;
+
+ modal.appendChild(title);
+ modal.appendChild(body);
+ modalBackground.appendChild(modal);
+ document.body.appendChild(modalBackground);
+
+ this.bg = modalBackground;
+ this.modal = modal;
+ this.title = title;
+ this.body = body;
+
+ this.registerEvents();
+ }
+
+ /**
+ * Register event to close if clicked outside of modal
+ * Clicking on the modal itself should not close it though
+ */
+ registerEvents() {
+ this.modal.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ this.bg.addEventListener('click', () => {
+ this.close();
+ });
+ }
+
+ /**
+ * Fades modal out and removes it from the flow of the document
+ */
+ close() {
+ this.bg.classList.add('hidden');
+ this.bg.addEventListener('transitionend', () => {
+ document.body.removeChild(this.bg);
+ });
+ }
+
+ /**
+ * Puts text in the body
+ * @param {string} text Text to put into the body
+ */
+ setBodyText(text) {
+ this.body.textContent = text;
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/components/notification-banner.js b/WebInterface/src/js/modules/ui/components/notification-banner.js
new file mode 100644
index 0000000..a527725
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/components/notification-banner.js
@@ -0,0 +1,133 @@
+/**
+ * Object containing a message for the notification banner
+ */
+class BannerItem {
+ /**
+ * Creates new Banner Message Items
+ * @param {String} name Name the message will be referenced under
+ * @param {String} content Content, either formatted as plain text or html
+ * @param {Boolean} html Is content formatted as html?
+ */
+ constructor(name, content, html) {
+ this.name = name;
+ this.content = content;
+ this.html = html;
+ }
+}
+
+/**
+ * Class for controlling the Notification banner
+ */
+export default class BannerController {
+ /**
+ * Creates references to objects and hides notification banner
+ * @param {Interface} iface Interface to receive comm. from
+ * @param {string} bannerId ID of Notification Banner
+ * @param {string} textP ID of Notification Banner text field
+ * @param {string} dismissBtn ID of dismiss button
+ * @param {string} badge ID of badge (# of notifications)
+ */
+ constructor(iface, bannerId, textP, dismissBtn, badge) {
+ iface.addObject(this, 'notifications', ['show', 'hide']);
+ this.iface = iface;
+
+ this.ids = {bannerId, textP, dismissBtn, badge};
+ }
+
+ /**
+ * Initializes the Banner in the DOM
+ */
+ initialize() {
+ this.banner = document.getElementById(this.ids.bannerId);
+ this.bannerText = document.getElementById(this.ids.textP);
+ this.dismissBtn = document.getElementById(this.ids.dismissBtn);
+ this.notificationBadge = document.getElementById(this.ids.badge);
+ this.bannerMsgs = [];
+
+ this.banner.classList.add('hidden'); // Hide banner by default
+ this.registerEvents();
+ }
+
+ /**
+ * Registers events for notification banner
+ */
+ registerEvents() {
+ this.registerDismissEvent();
+ }
+
+ /**
+ * Registers dismissing via dismiss button
+ */
+ registerDismissEvent() {
+ this.dismissBtn.addEventListener('click', () => {
+ this.dismissCurrent();
+ });
+ }
+
+ /**
+ * Pushes a new message to the notification banner and shows it
+ * @param {string} name Name to register notification (referenced in hide)
+ * @param {string} text Notification text
+ */
+ show(name, text) {
+ let bannerItem = new BannerItem(name, text, false);
+ this.bannerMsgs.push(bannerItem);
+
+ this.update();
+ }
+
+ /**
+ * Removes notification from banner
+ * @param {string} name The name the notification was registered under
+ */
+ hide(name) {
+ if (name) this.bannerMsgs = this.bannerMsgs.filter(elt => elt.name != name);
+ else this.bannerMsgs = [];
+
+ this.update();
+ }
+
+ /**
+ * Dismisses the currently shown message
+ */
+ dismissCurrent() {
+ this.hide(this.current);
+ }
+
+ /**
+ * Updates the notification banner with the most recent message
+ */
+ update() {
+ if (this.bannerMsgs.length === 0) {
+ this.banner.classList.add('hidden');
+ return;
+ }
+
+ const lastNotification = this.bannerMsgs[this.bannerMsgs.length - 1];
+ const name = lastNotification.name;
+ const text = lastNotification.content;
+ const isHtml = lastNotification.html;
+ this.banner.classList.remove('hidden');
+
+ if (isHtml) this.bannerText.innerHTML = text;
+ else this.bannerText.innerText = text;
+
+ this.current = name;
+ this.updateNotificationBadge();
+ }
+
+ /**
+ * Updates the notification badge number
+ */
+ updateNotificationBadge() {
+ if (this.bannerMsgs.length < 2) {
+ this.notificationBadge.classList.add('hidden');
+ } else if (this.bannerMsgs.length > 9) {
+ this.notificationBadge.classList.remove('hidden');
+ this.notificationBadge.textContent = '∞';
+ } else {
+ this.notificationBadge.classList.remove('hidden');
+ this.notificationBadge.textContent = this.bannerMsgs.length.toString();
+ }
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/components/router.js b/WebInterface/src/js/modules/ui/components/router.js
new file mode 100644
index 0000000..c01c21b
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/components/router.js
@@ -0,0 +1,44 @@
+/**
+ * Class for routing between pages
+ */
+export default class Router {
+ /**
+ * @param {Interface} iface Interface for comm. with other objects
+ */
+ constructor(iface) {
+ iface.addObject(this, 'serverListing', ['routePlay']);
+ this.iface = iface;
+ }
+
+ /**
+ * Routes to the play page
+ * @param {HubConnection} connection Connection to the server
+ */
+ routePlay(connection) {
+ window.history.pushState('object or string', 'Game Page',
+ 'play#game=' + this.serverName);
+ fetch('play').then((response) => {
+ response.text().then((htmlString) => {
+ // Replace all references, since we're starting one level farther up
+ htmlString = htmlString.replace(/\.\.\//g, './');
+ htmlString = /<body>((.)|(\n))*<\/body>/g.exec(htmlString)[0];
+ htmlString = htmlString.replace(/<script src=".*"><\/script>/, '');
+ htmlString = htmlString.replace(
+ /<remove_if_redirected>((.)|\n)*?<\/remove_if_redirected>/g, '');
+ document.body.innerHTML = htmlString;
+
+ let stylesheet = document.createElement('link');
+ stylesheet.rel = 'stylesheet';
+ stylesheet.type = 'text/css';
+ stylesheet.href = './style/play.css';
+ document.head.appendChild(stylesheet);
+
+
+ this.iface.callMethod('uiMananger', 'initPlay');
+ for (let ui of this.pageUI) {
+ ui.refresh();
+ }
+ });
+ });
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/components/server-listing.js b/WebInterface/src/js/modules/ui/components/server-listing.js
new file mode 100644
index 0000000..2af56ac
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/components/server-listing.js
@@ -0,0 +1,91 @@
+/**
+ * Class for handling the server list
+ */
+export default class ServerListing {
+ /**
+ * Creates reference to container
+ * @param {Interface} iface Interface for comm. with other objects
+ * @param {string} serverListId ID of the server list div
+ * @param {string} refreshBtnId ID of the refresh btn
+ */
+ constructor(iface, serverListId, refreshBtnId) {
+ this.ids = {serverListId, refreshBtnId};
+
+ iface.addObject(this, 'serverListing', ['flushElements', 'addElements']);
+ this.iface = iface;
+ }
+
+ /**
+ * Initializes Server List DOM Element
+ */
+ initialize() {
+ this.serverListing = document.getElementById(this.ids.serverListId);
+ this.refreshBtn = document.getElementById(this.ids.refreshBtnId);
+ this.registerEvents();
+ }
+
+ /**
+ * Registers events associated with server list UI
+ */
+ registerEvents() {
+ this.registerRefreshEvent();
+ }
+
+ /**
+ * Registers event for pushing the refresh button
+ */
+ registerRefreshEvent() {
+ this.refreshBtn.addEventListener('click', () => {
+ this.iface.callMethod('listServers', 'listServers');
+ });
+ }
+
+ /**
+ * Removes all elements currently in the server listing
+ */
+ flushElements() {
+ this.serverListing.innerHTML = '';
+ }
+
+ /**
+ * Populates servers from a given array of games
+ * @param {array} array Array of available games
+ */
+ addElements(array) {
+ for (let server of array) {
+ const name = server['name'];
+ const playerAmount = server['userCount'];
+
+ let serverDiv = document.createElement('div');
+ let nameSpan = document.createElement('span');
+ let rightAlignDiv = document.createElement('div');
+ let onlineDot = document.createElement('div');
+ let playerCountSpan = document.createElement('span');
+ let playerCountStaticSpan = document.createElement('span');
+ let joinButton = document.createElement('button');
+ serverDiv.className = 'server';
+ nameSpan.className = 'server-name';
+ rightAlignDiv.className = 'right-aligned-items';
+ onlineDot.className = 'player-count-dot';
+ playerCountSpan.className = 'player-count';
+ playerCountStaticSpan.className = 'player-count-static';
+ joinButton.className = 'btn join-btn';
+ joinButton.id = 'join';
+ nameSpan.textContent = name;
+ playerCountSpan.textContent = playerAmount;
+ playerCountStaticSpan.textContent = 'Spieler online';
+ joinButton.textContent = 'Beitreten';
+ joinButton.addEventListener('click', () => {
+ this.iface.callMethod('login', 'showLogin', name);
+ });
+
+ rightAlignDiv.appendChild(onlineDot);
+ rightAlignDiv.appendChild(playerCountSpan);
+ rightAlignDiv.appendChild(playerCountStaticSpan);
+ rightAlignDiv.appendChild(joinButton);
+ serverDiv.appendChild(nameSpan);
+ serverDiv.appendChild(rightAlignDiv);
+ this.serverListing.appendChild(serverDiv);
+ }
+ }
+}
diff --git a/WebInterface/src/js/modules/ui/uiManager.js b/WebInterface/src/js/modules/ui/uiManager.js
new file mode 100644
index 0000000..ddbb152
--- /dev/null
+++ b/WebInterface/src/js/modules/ui/uiManager.js
@@ -0,0 +1,60 @@
+import About from './collections/about';
+import Login from './collections/login';
+import Play from './collections/play';
+
+/**
+ * Controller class for Page UI
+ */
+export default class UIManager {
+ /**
+ * Initializes new UI Manager
+ * @param {Interface} iface Interface for inter-object communication
+ */
+ constructor(iface) {
+ this.currentUI = null;
+
+ iface.addObject(this, 'uiMananger', ['initAbout', 'initLogin', 'initPlay']);
+ this.iface = iface;
+ }
+
+ /**
+ * Initializes UI Components of About Page
+ */
+ initAbout() {
+ this.clearComponents();
+ this.about = new About(this.iface);
+ this.currentUI = 'about';
+ }
+
+ /**
+ * Initializes UI Components of Login page
+ */
+ initLogin() {
+ this.clearComponents();
+ this.login = new Login(this.iface);
+ this.currentUI = 'login';
+ }
+
+ /**
+ * Initializes UI Components of Play page
+ */
+ initPlay() {
+ this.clearComponents();
+ this.play = new Play(this.iface);
+ this.currentUI = 'play';
+ }
+
+ /**
+ * Clears currently loaded components
+ */
+ clearComponents() {
+ switch (this.currentUI) {
+ case null: return;
+ case 'about': this.about = null; break;
+ case 'login': this.login = null; break;
+ case 'play': this.play = null; break;
+ }
+
+ this.currentUI = null;
+ }
+}
diff --git a/WebInterface/src/js/play.js b/WebInterface/src/js/play.js
new file mode 100644
index 0000000..93508b3
--- /dev/null
+++ b/WebInterface/src/js/play.js
@@ -0,0 +1,12 @@
+import Interface from './modules/interface';
+import UIManager from './modules/ui/uiManager';
+import Networker from './modules/networking/networker';
+
+const SERVERURL = 'https://kobert.dev/api/login';
+
+let iface = new Interface();
+let uiMan = new UIManager(iface);
+uiMan.initPlay();
+// Create Network Manager as well
+
+// TODO: Implement login from the play page
diff --git a/WebInterface/src/js/src_old/modules/playModule.js b/WebInterface/src/js/src_old/modules/playModule.js
new file mode 100644
index 0000000..931f598
--- /dev/null
+++ b/WebInterface/src/js/src_old/modules/playModule.js
@@ -0,0 +1,46 @@
+// TODO: Handle disconnect
+
+/**
+ * Handles ingame networking;
+ */
+export default class GameClient {
+ /**
+ * Defines basic attributes
+ * @param {string} user The username of the player
+ * @param {HubConnection} connection Already established connection to the
+ * server
+ */
+ constructor(user, connection) {
+ this.user = user;
+ this.connection = connection;
+ }
+
+ /**
+ * Registers chat html component
+ * @param {string} chatId Id of chat component
+ */
+ registerChat(chatId) {
+ this.chat = document.getElementById(chatId);
+ this.messageList = this.chat.querySelector('#message-list');
+ this.messageInput = this.chat.querySelector('#input-message');
+ this.messageSend = this.chat.querySelector('#send-message');
+
+ this.connection.on('ReceiveMessage', (user, message) => {
+ let msg = message.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ let encodedMsg = user + ' sagt: ' + msg;
+
+ let messageP = document.createElement('p');
+ messageP.class = 'message';
+ messageP.textContent = encodedMsg;
+
+ this.messageList.appendChild(messageP);
+ });
+
+ this.messageSend.addEventListener('click', () => {
+ let message = this.messageInput.value;
+ this.connection.invoke('SendMessage', this.user, message);
+ });
+ }
+}
diff --git a/WebInterface/src/js/src_old/modules/server-client.js b/WebInterface/src/js/src_old/modules/server-client.js
new file mode 100644
index 0000000..2f712b5
--- /dev/null
+++ b/WebInterface/src/js/src_old/modules/server-client.js
@@ -0,0 +1,93 @@
+import * as signalR from '@aspnet/signalr';
+import ServerListing from './ui/server-listing.js';
+
+/**
+ * Class for communication to server
+ */
+export default class ServerClient {
+ /**
+ * Creates new connection
+ * @param {string} url URL of server running signalR
+ * @param {string} serverListingId HTML ID of server-listing element,
+ * to populate with available games
+ * @param {BannerController} notifications Notification Manager
+ * @param {array} ui UI Elements to reload on login
+ * @param {boolean} [debug=false] Enable debug output?
+ */
+ constructor(url, serverListingId, notifications, ui, debug = false) {
+ this.ui = ui;
+ const connectionBuilder = new signalR.HubConnectionBuilder()
+ .withUrl(url);
+
+ if (debug) {
+ connectionBuilder.configureLogging(signalR.LogLevel.Debug);
+ } else {
+ connectionBuilder.configureLogging(signalR.LogLevel.Error);
+ }
+
+ this.connection = connectionBuilder.build();
+ this.connection.start()
+ .then(() => this.loadServers()) // Load games list, once connected
+ .catch((err) => console.error(err.toString()));
+
+ // Initialize refreshing (blocks new refreshes if true)
+ this.refreshing = false;
+
+ this.serverListing = new ServerListing(serverListingId, notifications);
+ }
+
+ /**
+ * Requests list of avalable games on the server
+ */
+ loadServers() {
+ if (this.refreshing) return; // If already refreshing, no new request
+
+ this.connection.on('ListGroups', (groups) => {
+ // Populate server listing
+ this.serverListing.flushElements();
+ this.serverListing.addElements(groups, this, this.ui);
+ this.connection.off('ListGroups');
+
+ this.refreshing = false;
+ });
+
+ this.connection.invoke('GetGroups')
+ .catch((err) => {
+ this.refreshing = false;
+ console.error(err.toString());
+ });
+ this.refreshing = true;
+ }
+
+ /**
+ * Sends a game creating request to the server
+ * @param {string} name Name of the new game
+ * @param {string} password Password
+ */
+ createServer(name, password) {
+ // TODO: Create
+ }
+
+ /**
+ * Sends a login request
+ * @param {string} group Group name to join
+ * @param {string} password Password to send as SHA-256 Base64 String
+ * @param {string} username Display name to use
+ * @param {ServerClient~loginCallback} callback Callback function to use
+ */
+ sendLogin(group, password, username, callback) {
+ this.connection.on('LoginResponse', (result) => {
+ callback(result, this.connection);
+ this.connection.off('LoginResponse');
+ });
+ this.connection.invoke('Login', group, username, password);
+ }
+}
+
+/**
+ * Callback to call with response to login request
+ * @callback ServerClient~loginCallback
+ * @param {number} result 0: Success, 1: PasswordError, 2:UsernameTaken,
+ * 3:Unknown Error
+ * @param {ConnectionHub} connection Connection to the server
+ */
diff --git a/WebInterface/src/js/src_old/modules/ui/login-modal.js b/WebInterface/src/js/src_old/modules/ui/login-modal.js
new file mode 100644
index 0000000..13de78e
--- /dev/null
+++ b/WebInterface/src/js/src_old/modules/ui/login-modal.js
@@ -0,0 +1,181 @@
+import Modal from './modal.js';
+import '../hash.js';
+
+/**
+ * Class to implement a login modal from the parent modal class
+ */
+export default class LoginModal extends Modal {
+ /**
+ * Creates necessary elements for login modal
+ * @param {string} serverName Name of the server, used for login and displayed
+ * in title
+ * @param {ServerClient} serverClient Server client object used to send the
+ * login
+ * @param {BannerController} notificationManager Object controlling the main
+ * notification banners
+ * @param {array} ui UI elements to call refresh method on after login
+ */
+ constructor(serverName, serverClient, notificationManager, ui) {
+ super(serverName);
+ this.serverName = serverName;
+ this.serverClient = serverClient;
+ this.notificationManager = notificationManager;
+ this.pageUI = ui;
+
+ let passBox = document.createElement('div');
+ let nameBox = document.createElement('div');
+ let sendBox = document.createElement('div');
+
+ let passwordLabel = document.createElement('label');
+ let passwordInput = document.createElement('input');
+ let passwordInvalid = document.createElement('span');
+ passwordLabel.setAttribute('for', 'password-input');
+ passwordLabel.textContent = 'Passwort:';
+ passwordLabel.title = 'Das Passwort des Spiels';
+ passwordInput.id = 'password-input';
+ passwordInput.type = 'password';
+ passwordInput.placeholder = 'Passwort';
+ passwordInput.title = 'Das Passwort des Spiels';
+ passwordInvalid.className = 'invalid hidden';
+ passwordInvalid.textContent = 'Das eingegebene Passwort ist falsch.';
+
+ let nameLabel = document.createElement('label');
+ let nameInput = document.createElement('input');
+ let nameInvalid = document.createElement('span');
+ nameLabel.setAttribute('for', 'name-input');
+ nameLabel.textContent = 'Benutzername:';
+ nameLabel.title = 'Dein Anzeigename';
+ nameInput.id = 'name-input';
+ nameInput.type = 'text';
+ nameInput.autocomplete = 'on';
+ nameInput.placeholder = 'Name';
+ nameInput.title = 'Dein Anzeigename';
+ nameInvalid.className = 'invalid hidden';
+ nameInvalid.textContent =
+ 'Der eingegebene Nutzername ist bereits vergeben.';
+
+ let sendButton = document.createElement('button');
+ sendButton.className = 'btn';
+ sendButton.textContent = 'Login';
+ sendButton.id = 'login-button';
+
+ passBox.appendChild(passwordLabel);
+ passBox.appendChild(passwordInput);
+ passBox.appendChild(passwordInvalid);
+ nameBox.appendChild(nameLabel);
+ nameBox.appendChild(nameInput);
+ nameBox.appendChild(nameInvalid);
+ sendBox.appendChild(sendButton);
+
+ this.body.appendChild(passBox);
+ this.body.appendChild(nameBox);
+ this.body.appendChild(sendBox);
+
+ this.nameInput = nameInput;
+ this.passwordInput = passwordInput;
+ this.loginButton = sendButton;
+
+ this.passwordInvalid = passwordInvalid;
+ this.nameInvalid = nameInvalid;
+
+ this.registerLoginBtn();
+ }
+
+ /**
+ * Registers event to send login, on button press
+ */
+ registerLoginBtn() {
+ let eventListener;
+ let loginCallBack = (result, connection) => {
+ console.log(result);
+ if (result == 0) {
+ this.redirectToPlay(connection);
+ this.close();
+ } else if (result == 1) {
+ this.invalid('Name');
+ this.loginButton.addEventListener('click', eventListener);
+ } else if (result == 2) {
+ this.invalid('Pass');
+ this.loginButton.addEventListener('click', eventListener);
+ } else {
+ this.notificationManager.show('unknownLoginErr',
+ 'Ein unbekannter Fehler ist aufgetreten');
+ this.close();
+ }
+ };
+
+ eventListener = () => {
+ this.invalid(); // Remove 'invalid' messages
+ this.loginButton.removeEventListener('click', eventListener);
+ this.userName = this.nameInput.value;
+ this.passwordInput.value.getHash()
+ .then((result) => {
+ this.serverClient.sendLogin(this.serverName, result,
+ this.userName, loginCallBack);
+ });
+ };
+ this.loginButton.addEventListener('click', eventListener);
+ }
+
+ /**
+ * Displays text under invalid password / username
+ * @param {string} inv Which field to display under (Pass / Name)
+ * Blank inv will hide both
+ */
+ invalid(inv) {
+ this.body.classList.remove('frst-warning');
+ this.body.classList.remove('scnd-warning');
+
+ this.passwordInvalid.classList.add('hidden');
+ this.nameInvalid.classList.add('hidden');
+
+ this.passwordInput.style.borderColor = 'none';
+ this.nameInput.style.borderColor = 'none';
+
+ if (inv == 'Pass') {
+ this.body.classList.add('frst-warning');
+ this.passwordInvalid.classList.remove('hidden');
+ this.passwordInput.style.borderColor = '#ef5350';
+ } else if (inv == 'Name') {
+ this.body.classList.add('scnd-warning');
+ this.nameInvalid.classList.remove('hidden');
+ this.nameInput.style.borderColor = '#ef5350';
+ }
+ }
+
+ /**
+ * Loads play site
+ * @param {HubConnection} connection Connection to the server
+ */
+ redirectToPlay(connection) {
+ window.history.pushState('object or string', 'Game Page',
+ 'play#game=' + this.serverName);
+ fetch('play').then((response) => {
+ response.text().then((htmlString) => {
+ // Replace all references, since we're starting one level farther up
+ htmlString = htmlString.replace(/\.\.\//g, './');
+ htmlString = /<body>((.)|(\n))*<\/body>/g.exec(htmlString)[0];
+ htmlString = htmlString.replace(/<script src=".*"><\/script>/, '');
+ htmlString = htmlString.replace(
+ /<remove_if_redirected>((.)|\n)*?<\/remove_if_redirected>/g, '');
+ document.body.innerHTML = htmlString;
+
+ let stylesheet = document.createElement('link');
+ stylesheet.rel = 'stylesheet';
+ stylesheet.type = 'text/css';
+ stylesheet.href = './style/play.css';
+ document.head.appendChild(stylesheet);
+
+ for (let ui of this.pageUI) {
+ ui.refresh();
+ }
+
+ import(/* webpackChunkName: "/playModule" */ '../playModule')
+ .then(({default: GameClient}) => {
+ let gameClient = new GameClient(this.userName, connection);
+ gameClient.registerChat('chat');
+ });
+ });
+ });
+ }
+}
diff --git a/WebInterface/src/js/src_old/modules/ui/modal.js b/WebInterface/src/js/src_old/modules/ui/modal.js
new file mode 100644
index 0000000..10a1be5
--- /dev/null
+++ b/WebInterface/src/js/src_old/modules/ui/modal.js
@@ -0,0 +1,67 @@
+/**
+ * Parent class to create Modals on the screen
+ * Contains no content, as that is implemented by child classes
+ */
+export default class Modal {
+ /**
+ * Creates a new modal with a title and empty content
+ * @param {string} titleString Title to show at the top of the modal
+ */
+ constructor(titleString) {
+ let modalBackground = document.createElement('div');
+ let modal = document.createElement('div');
+ let title = document.createElement('h1');
+ let body = document.createElement('div');
+
+ modalBackground.className = 'modal-container';
+ modal.className = 'modal';
+ title.className = 'modal-title';
+ body.className = 'modal-body';
+
+ title.textContent = titleString;
+
+ modal.appendChild(title);
+ modal.appendChild(body);
+ modalBackground.appendChild(modal);
+ document.body.appendChild(modalBackground);
+
+ this.bg = modalBackground;
+ this.modal = modal;
+ this.title = title;
+ this.body = body;
+
+ this.registerEvents();
+ }
+
+ /**
+ * Register event to close if clicked outside of modal
+ * Clicking on the modal itself should not close it though
+ */
+ registerEvents() {
+ this.modal.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ this.bg.addEventListener('click', () => {
+ this.close();
+ });
+ }
+
+ /**
+ * Fades modal out and removes it from the flow of the document
+ */
+ close() {
+ this.bg.classList.add('hidden');
+ this.bg.addEventListener('transitionend', () => {
+ document.body.removeChild(this.bg);
+ });
+ }
+
+ /**
+ * Puts text in the body
+ * @param {string} text Text to put into the body
+ */
+ setBodyText(text) {
+ this.body.textContent = text;
+ }
+}
diff --git a/WebInterface/src/js/src_old/modules/ui/server-listing.js b/WebInterface/src/js/src_old/modules/ui/server-listing.js
new file mode 100644
index 0000000..78ca323
--- /dev/null
+++ b/WebInterface/src/js/src_old/modules/ui/server-listing.js
@@ -0,0 +1,67 @@
+import LoginModal from './login-modal.js';
+
+/**
+ * Class for handling the server list
+ */
+export default class ServerListing {
+ /**
+ * Creates reference to container
+ * @param {string} serverListId ID of the server list div
+ * @param {BannerController} notifications Notification Manager
+ */
+ constructor(serverListId, notifications) {
+ this.serverListing = document.getElementById(serverListId);
+ this.notifications = notifications;
+ }
+
+ /**
+ * Removes all elements currently in the server listing
+ */
+ flushElements() {
+ this.serverListing.innerHTML = '';
+ }
+
+ /**
+ * Populates servers from a given array of games
+ * @param {array} array Array of available games
+ * @param {ServerClient} serverClient Server Client to handle login
+ * @param {array} ui UI Elements to reload after login
+ */
+ addElements(array, serverClient, ui) {
+ for (let server of array) {
+ const name = server['name'];
+ const playerAmount = server['userCount'];
+
+ let serverDiv = document.createElement('div');
+ let nameSpan = document.createElement('span');
+ let rightAlignDiv = document.createElement('div');
+ let onlineDot = document.createElement('div');
+ let playerCountSpan = document.createElement('span');
+ let playerCountStaticSpan = document.createElement('span');
+ let joinButton = document.createElement('button');
+ serverDiv.className = 'server';
+ nameSpan.className = 'server-name';
+ rightAlignDiv.className = 'right-aligned-items';
+ onlineDot.className = 'player-count-dot';
+ playerCountSpan.className = 'player-count';
+ playerCountStaticSpan.className = 'player-count-static';
+ joinButton.className = 'btn join-btn';
+ joinButton.id = 'join';
+ nameSpan.textContent = name;
+ playerCountSpan.textContent = playerAmount;
+ playerCountStaticSpan.textContent = 'Spieler online';
+ joinButton.textContent = 'Beitreten';
+ joinButton.addEventListener('click', () => {
+ new LoginModal(name, serverClient, this.notifications, ui);
+ });
+
+ rightAlignDiv.appendChild(onlineDot);
+ rightAlignDiv.appendChild(playerCountSpan);
+ rightAlignDiv.appendChild(playerCountStaticSpan);
+ rightAlignDiv.appendChild(joinButton);
+ serverDiv.appendChild(nameSpan);
+ serverDiv.appendChild(rightAlignDiv);
+ this.serverListing.appendChild(serverDiv);
+ }
+ }
+}
diff --git a/WebInterface/src/style/about.scss b/WebInterface/src/style/about.scss
new file mode 100644
index 0000000..3b45674
--- /dev/null
+++ b/WebInterface/src/style/about.scss
@@ -0,0 +1,43 @@
+@import 'partials/base';
+@import 'partials/backdrop/base';
+@import 'partials/backdrop/menu';
+@import 'partials/front-layer/base';
+@import 'partials/front-layer/copyright';
+
+
+h1, h2, p, a {
+ margin: 0.5rem 1rem;
+}
+
+h1, h2 {
+ margin-top: 1rem;
+ text-align: center;
+}
+
+.front-layer {
+ padding-bottom: 5rem;
+ display: flex;
+}
+
+.about-container {
+ flex-grow: 1;
+ overflow-y: auto;
+ margin: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ .about {
+ flex-grow: 1;
+ }
+
+ .contact {
+ margin: 1rem;
+ display: grid;
+ grid-template-columns: 1fr 5fr;
+ grid-template-rows: 1fr 1fr 1fr 1fr 1fr;
+ flex-direction: column;
+
+ h2 {
+ grid-column: 1/3;
+ }
+ }
+}
diff --git a/WebInterface/src/style/index.scss b/WebInterface/src/style/index.scss
new file mode 100644
index 0000000..04c896a
--- /dev/null
+++ b/WebInterface/src/style/index.scss
@@ -0,0 +1,10 @@
+@import 'partials/base';
+@import 'partials/btn';
+@import 'partials/backdrop/base';
+@import 'partials/backdrop/menu';
+@import 'partials/front-layer/base';
+@import 'partials/front-layer/notifications';
+@import 'partials/front-layer/server-listing';
+@import 'partials/front-layer/copyright';
+@import 'partials/modal/base';
+@import 'partials/modal/login';
diff --git a/WebInterface/src/style/partials/_base.scss b/WebInterface/src/style/partials/_base.scss
new file mode 100644
index 0000000..5f300fd
--- /dev/null
+++ b/WebInterface/src/style/partials/_base.scss
@@ -0,0 +1,20 @@
+@import 'colors';
+
+html,body {
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ font-family: 'Roboto', sans-serif;
+ font-display: swap;
+ overflow: hidden;
+ background-color: $primary;
+ color: $primary-text;
+ user-select: none;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ background-color: $secondary;
+ position: relative;
+}
diff --git a/WebInterface/src/style/partials/_btn.scss b/WebInterface/src/style/partials/_btn.scss
new file mode 100644
index 0000000..cf5ee33
--- /dev/null
+++ b/WebInterface/src/style/partials/_btn.scss
@@ -0,0 +1,75 @@
+@import 'colors';
+
+.btn {
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ margin: 0;
+ font-size: 1rem;
+ font-family: 'Roboto Condensed', sans-serif;
+ font-weight: bold;
+ display: inline-block;
+ background-color: $secondary;
+ color: $secondary-text;
+ text-transform: uppercase;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
+ cursor: pointer;
+ letter-spacing: 0.25rem;
+
+ background-position: center;
+ transition: background 800ms ease, box-shadow 100ms ease, color 200ms ease;
+
+ &:hover {
+ background: $secondary-dark radial-gradient(circle, transparent 1%, $secondary-dark 1%) center/15000%;
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
+ }
+
+ &:active {
+ background-color: $secondary-light;
+ background-size: 100%;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12);
+ transition: background 0s, box-shadow 0s;
+ }
+
+ &:disabled {
+ color: $secondary-text-disabled;
+ background: $secondary-disabled;
+ box-shadow: none;
+ transition: background 200ms ease, box-shadow 200ms ease, color 200ms ease;
+ }
+}
+
+.text-btn {
+ padding: 8px;
+ margin: 0;
+ font-size: 1rem;
+ font-family: 'Roboto Condensed', sans-serif;
+ font-weight: bold;
+ display: inline-block;
+ color: $secondary-text;
+ background-color: rgba(0, 0, 0, 0);
+ text-transform: uppercase;
+ box-shadow: none;
+
+ background-position: center;
+ transition: background 800ms ease, box-shadow 100ms ease, color 200ms ease;
+
+ &:hover {
+ background: $secondary-dark-transparent radial-gradient(circle, transparent 1%, $secondary-dark-transparent 1%) center/15000%;
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
+ }
+
+ &:active {
+ background-color: $secondary-light;
+ background-size: 100%;
+ box-shadow: none;
+ transition: background 0s, box-shadow 0s;
+ }
+
+ &:disabled {
+ background: rgba(0,0,0,0);
+ color: $secondary-text-disabled;
+ box-shadow: none;
+ transition: background 200ms ease, box-shadow 200ms ease, color 200ms ease;
+ }
+}
diff --git a/WebInterface/src/style/partials/_colors.scss b/WebInterface/src/style/partials/_colors.scss
new file mode 100644
index 0000000..4683325
--- /dev/null
+++ b/WebInterface/src/style/partials/_colors.scss
@@ -0,0 +1,18 @@
+$primary: #212121;
+$primary-disabled: #21212161;
+$primary-light: #484848;
+$primary-dark: #000000;
+$primary-text: #ffffff;
+$primary-text-disabled: #ffffff61;
+$primary-warning-text: #ef5350;
+$secondary: #546e7a;
+$secondary-disabled: #546e7a61;
+$secondary-half-dark: #3e5864;
+$secondary-dark: #29434e;
+$secondary-dark-transparent: #29434e61;
+$secondary-light: #819ca9;
+$secondary-light-transparent: #819ca961;
+$secondary-text: #ffffff;
+$secondary-text-disabled: #ffffff61;
+
+$online-green: #339933;
diff --git a/WebInterface/src/style/partials/_game.scss b/WebInterface/src/style/partials/_game.scss
new file mode 100644
index 0000000..74b566b
--- /dev/null
+++ b/WebInterface/src/style/partials/_game.scss
@@ -0,0 +1,13 @@
+@import './colors';
+
+.game-window {
+ width: calc(100% - 2rem);
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ background-color: $secondary-light;
+ margin: 1rem;
+ margin-bottom: 2rem;
+ border-style: none;
+ border-radius: 8px;
+}
diff --git a/WebInterface/src/style/partials/backdrop/_base.scss b/WebInterface/src/style/partials/backdrop/_base.scss
new file mode 100644
index 0000000..1b7a924
--- /dev/null
+++ b/WebInterface/src/style/partials/backdrop/_base.scss
@@ -0,0 +1,58 @@
+@import '../colors';
+
+.backdrop {
+ background-color: $secondary;
+ color: $secondary-text;
+ font-size: 1rem;
+
+ .header-bar {
+ display: flex;
+ align-items: center;
+
+ @media (max-height: 550px) {
+ margin-top: 0.125rem;
+ }
+
+ .menu-icon {
+ background-image: url("../ressources/menu.png");
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 36px;
+ height: 36px;
+ padding: 4px;
+ margin: 1rem;
+ display: inline-block;
+ border: none;
+ border-radius: 32px;
+ transition: background-color 100ms ease;
+
+ @media (max-height: 550px) {
+ padding: 0;
+ margin: 0.125rem;
+ }
+
+ &:hover {
+ background-color: $secondary-dark;
+ }
+
+ &.open {
+ background-image: url("../ressources/menu_close.png");
+ }
+ }
+
+ .header {
+ margin: 0;
+ padding: 16px;
+ text-align: center;
+ flex-grow: 1;
+
+ @media (min-width: 450px) {
+ margin-right: 56px;
+ }
+
+ @media (max-height: 550px) {
+ padding: 0;
+ }
+ }
+ }
+}
diff --git a/WebInterface/src/style/partials/backdrop/_menu.scss b/WebInterface/src/style/partials/backdrop/_menu.scss
new file mode 100644
index 0000000..26833d5
--- /dev/null
+++ b/WebInterface/src/style/partials/backdrop/_menu.scss
@@ -0,0 +1,43 @@
+@import '../colors';
+
+.backdrop {
+ .menu-actions {
+ box-sizing: border-box;
+ transition: max-height 200ms ease, color 200ms ease, background-color 100ms ease;;
+ position: relative;
+ max-height: 16rem;
+ margin: 0 1rem;
+
+ .menu-option {
+ color: $primary-text;
+ text-decoration: none;
+ box-sizing: border-box;
+ display: inline-block;
+ font-size: 1.5rem;
+ width: 100%;
+ text-align: center;
+ padding: 1rem;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $secondary-half-dark;
+ }
+
+ &.active {
+ background-color: $secondary-dark;
+ }
+ }
+
+ &.hidden {
+ max-height: 0;
+ color: rgba(0,0,0,0);
+
+ .menu-option {
+ color: rgba(0,0,0,0);
+ background-color: rgba(0,0,0,0) !important;
+ }
+ }
+ }
+}
diff --git a/WebInterface/src/style/partials/front-layer/_base.scss b/WebInterface/src/style/partials/front-layer/_base.scss
new file mode 100644
index 0000000..6a2aa9a
--- /dev/null
+++ b/WebInterface/src/style/partials/front-layer/_base.scss
@@ -0,0 +1,23 @@
+.front-layer {
+ @keyframes start {
+ from {top: 100vh;}
+ to {top: 0;}
+ }
+
+ position: relative;
+ border: none;
+ border-radius: 16px 16px 0 0;
+ min-height: 0;
+ height: 100%;
+
+ box-sizing: border-box;
+ background-color: white;
+ margin-top: 8px;
+ animation-name: start;
+ animation-duration: 1s;
+ animation-timing-function: ease;
+ color: black;
+
+ display: flex;
+ flex-direction: column;
+}
diff --git a/WebInterface/src/style/partials/front-layer/_copyright.scss b/WebInterface/src/style/partials/front-layer/_copyright.scss
new file mode 100644
index 0000000..31ac614
--- /dev/null
+++ b/WebInterface/src/style/partials/front-layer/_copyright.scss
@@ -0,0 +1,10 @@
+.front-layer{
+ .copyright-container {
+ box-sizing: border-box;
+ position: absolute;
+ width: 100%;
+ margin: 4px;
+ bottom: 0;
+ text-align: center;
+ }
+}
diff --git a/WebInterface/src/style/partials/front-layer/_notifications.scss b/WebInterface/src/style/partials/front-layer/_notifications.scss
new file mode 100644
index 0000000..d8e8159
--- /dev/null
+++ b/WebInterface/src/style/partials/front-layer/_notifications.scss
@@ -0,0 +1,84 @@
+@import '../colors';
+
+.front-layer {
+ .banner {
+ background-color: white;
+ border: none;
+ border-radius: 16px 16px 0 0;
+ display: flex;
+ margin: 0;
+ margin-bottom: 1rem;
+ padding-top: 1rem;
+ max-height: 10rem;
+ flex-direction: row;
+ flex-wrap: wrap;
+ transform-origin: top;
+ transition: max-height 200ms ease, transform 200ms ease, visibility 200ms step-start;
+ min-height: 3.5rem;
+ justify-items: center;
+ justify-content: center;
+
+ @media (max-height: 550px) {
+ position: absolute;
+ width: 100%;
+ box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
+ }
+
+ &.hidden {
+ transform: scaleY(0);
+ visibility: hidden;
+ max-height: 0;
+ transition: max-height 200ms ease, transform 200ms ease, visibility 200ms step-end;
+ min-height: 0;
+ margin-bottom: 0;
+ }
+
+ .notification-badge {
+ height: 1rem;
+ min-width: 1rem;
+ font-weight: bold;
+ color: $secondary-text;
+ font-size: 1rem;
+ padding: 0.5rem;
+ margin: 0.5rem 0 0.5rem 1rem;
+ text-align: center;
+ line-height: 1rem;
+ background-color: $secondary;
+ border: none;
+ border-radius: 50%;
+
+ &.hidden {
+ display: none;
+ }
+ }
+
+ .banner-text {
+ align-self: left;
+ margin: 1rem;
+ flex-grow: 100;
+ }
+
+ .btn-container{
+ display: flex;
+ flex-grow: 1;
+ text-align: right;
+
+
+ .banner-button {
+ color: $secondary-dark;
+ padding: 8px 16px;
+ margin: 0;
+ margin-right: 1rem;
+ letter-spacing: 0.125rem;
+ }
+ }
+
+ hr {
+ width: 100%;
+
+ @media (max-height: 550px) {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
diff --git a/WebInterface/src/style/partials/front-layer/_server-listing.scss b/WebInterface/src/style/partials/front-layer/_server-listing.scss
new file mode 100644
index 0000000..7e67178
--- /dev/null
+++ b/WebInterface/src/style/partials/front-layer/_server-listing.scss
@@ -0,0 +1,121 @@
+@import '../colors';
+
+.front-layer {
+ .server-listing {
+ box-sizing: border-box;
+ background-color: $primary;
+ color: $primary-text;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ border-style: none;
+ border-radius: 8px;
+ margin: 1rem;
+ margin-bottom: 2rem;
+ padding: 0.5rem;
+ padding-top: 0.25rem;
+ box-shadow: 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12), 0 5px 5px -3px rgba(0, 0, 0, .2);
+ h1 {
+ text-align: center;
+ @media (max-height: 550px) {
+ display: none;
+ }
+ }
+ hr {
+ width: 100%;
+ @media (max-height: 550px) {
+ display: none;
+ }
+ }
+
+ @media (max-height: 450px) {
+ padding-bottom: 0.125rem
+ }
+
+ .server-entries {
+ overflow-y: auto;
+ min-height: 0;
+
+ .server {
+ font-size: 1.25rem;
+ display: flex;
+ flex-direction: row;
+
+ @media (max-width: 1000px) {
+ flex-wrap: wrap;
+ }
+
+ align-items: center;
+ background-color: $primary-light;
+ padding: 0.5rem;
+ margin-bottom: 0.25rem;
+ border-style: none;
+ border-radius: 8px;
+
+ .server-name {
+ font-weight: bold;
+ letter-spacing: 0.125rem;
+ white-space: nowrap;
+ overflow: hidden;
+ margin: 0.5rem 0;
+ }
+
+ .right-aligned-items {
+ margin-left: auto;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+
+ .player-count-dot {
+ background-color: $online-green;
+ border-radius: 50%;
+ min-width: 1rem;
+ min-height: 1rem;
+ width: 1rem;
+ height: 1rem;
+ margin-right: 0.25rem;
+ }
+
+ .player-count {
+ font-family: 'Roboto Condensed', sans-serif;
+ }
+
+ .player-count-static {
+ @media (max-width: 1000px) {
+ display: none;
+ }
+
+ white-space: nowrap;
+ margin-left: 0.25rem;
+ letter-spacing: 0;
+ font-family: 'Roboto Condensed', sans-serif;
+ }
+
+ .join-btn {
+ margin-left: 0.5rem;
+ min-width: 12rem;
+ }
+ }
+ }
+ }
+
+ .button-container {
+ display: flex;
+ flex-direction: row-reverse;
+ margin-top: 1rem;
+ margin-right: 0.25rem;
+ min-height: 2.5rem;
+
+ @media (max-height: 450px) {
+ margin-top: 0.125rem;
+ min-height: 2rem;
+ }
+
+ .btn {
+ @media (max-width: 450px) {
+ letter-spacing: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/WebInterface/src/style/partials/game/_chat.scss b/WebInterface/src/style/partials/game/_chat.scss
new file mode 100644
index 0000000..89b63b3
--- /dev/null
+++ b/WebInterface/src/style/partials/game/_chat.scss
@@ -0,0 +1,28 @@
+@import '../colors';
+@import '../btn';
+
+.chat {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+
+ .messages {
+ flex-grow: 1;
+ background-color: white;
+ margin: 0.5rem;
+ }
+
+ .input-message {
+ display: flex;
+ margin: 0.5rem;
+
+ input {
+ font-size: 2rem;
+ flex-grow: 1;
+ }
+
+ .btn {
+ margin-left: 1rem;
+ }
+ }
+}
diff --git a/WebInterface/src/style/partials/modal/_base.scss b/WebInterface/src/style/partials/modal/_base.scss
new file mode 100644
index 0000000..006c241
--- /dev/null
+++ b/WebInterface/src/style/partials/modal/_base.scss
@@ -0,0 +1,35 @@
+@import '../colors';
+
+.modal-container {
+ position:absolute;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000000AA;
+ display: flex;
+ opacity: 1;
+ transition: opacity 200ms ease;
+
+ &.hidden {
+ opacity: 0;
+ }
+
+ .modal {
+ background-color: $primary;
+ margin: auto;
+ padding: 1rem;
+ border-style: none;
+ border-radius: 8px;
+ box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
+ }
+
+ .modal-title {
+ max-width: 85vw;
+ margin: 1rem;
+ text-align: center;
+ }
+
+ .modal-body {
+ margin: 0.25rem;
+ }
+}
diff --git a/WebInterface/src/style/partials/modal/_login.scss b/WebInterface/src/style/partials/modal/_login.scss
new file mode 100644
index 0000000..71adf61
--- /dev/null
+++ b/WebInterface/src/style/partials/modal/_login.scss
@@ -0,0 +1,55 @@
+.modal {
+ .modal-title {
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .modal-body {
+ display: grid;
+ grid-template-columns: 10em 1fr;
+ grid-template-rows: 1fr 1fr 1fr;
+ grid-row-gap: 0.5rem;
+
+ &.frst-warning {
+ grid-template-rows: 1fr 1rem 1fr 1fr;
+ }
+
+ &.scnd-warning {
+ grid-template-rows: 1fr 1fr 1rem 1fr;
+ }
+
+ div {
+ display: contents;
+ font-size: 1.25rem;
+
+ label {
+ margin-right: 2rem;
+ grid-column: 1 / 2;
+ line-height: 1.25em;
+ }
+
+ input {
+ grid-column: 2 / 3;
+ border-color: none;
+ }
+
+ span {
+ margin-top: -0.5rem;
+ grid-column: 1 / 3;
+ color: $primary-warning-text;
+ background-color: $primary;
+ font-size: 0.9rem;
+ line-height: 1.2rem;
+ padding: 0.25rem;
+
+ &.hidden {
+ display: none;
+ }
+ }
+
+ button {
+ grid-column: 1 / 3;
+ }
+ }
+ }
+}
diff --git a/WebInterface/src/style/play.scss b/WebInterface/src/style/play.scss
new file mode 100644
index 0000000..7286f74
--- /dev/null
+++ b/WebInterface/src/style/play.scss
@@ -0,0 +1,17 @@
+@import 'partials/base';
+@import 'partials/btn';
+@import 'partials/backdrop/base';
+@import 'partials/backdrop/menu';
+@import 'partials/front-layer/base';
+@import 'partials/front-layer/notifications';
+@import 'partials/front-layer/copyright';
+@import 'partials/modal/base';
+@import 'partials/modal/login';
+@import 'partials/game';
+@import 'partials/game/chat';
+
+remove_if_redirected {
+ width: 100vw;
+ height: 100vh;
+ text-align: center;
+}