diff options
author | Dennis Kobert <d-kobert@web.de> | 2019-06-11 23:53:30 +0200 |
---|---|---|
committer | Dennis Kobert <d-kobert@web.de> | 2019-06-11 23:53:30 +0200 |
commit | 3a3d0fc3d4733f8908e23a03f860d76340479ec4 (patch) | |
tree | cf4b82f61d01d2a24836e9820d73972436847982 /WebInterface/src | |
parent | c28c9fafa2c74b101f7ce777aac722dcdeecefc6 (diff) |
Reorganize Project structure
Diffstat (limited to 'WebInterface/src')
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, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); + 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; +} |