diff --git a/backend/src/main/java/achievements/Application.java b/backend/src/main/java/achievements/Application.java
index b622eff..971ab08 100644
--- a/backend/src/main/java/achievements/Application.java
+++ b/backend/src/main/java/achievements/Application.java
@@ -1,5 +1,6 @@
package achievements;
+import achievements.misc.Password;
import achievements.services.DbConnectionService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
diff --git a/backend/src/main/java/achievements/controllers/Controller.java b/backend/src/main/java/achievements/controllers/Controller.java
index 7595ba5..bd1a0d3 100644
--- a/backend/src/main/java/achievements/controllers/Controller.java
+++ b/backend/src/main/java/achievements/controllers/Controller.java
@@ -1,6 +1,7 @@
package achievements.controllers;
import achievements.data.Achievements;
+import achievements.data.User;
import achievements.data.Games;
import achievements.data.InternalError;
import achievements.services.DbService;
@@ -12,6 +13,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
+import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController
public class Controller {
@@ -62,4 +64,47 @@ public class Controller {
return new ResponseEntity("{}", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
+
+ /**
+ * Acceptable codes
+ * 0 => Success
+ * 1 => Email already registered
+ *
+ * -1 => Unknown error
+ */
+ @RequestMapping(value = "/create_user", method = POST, consumes = "application/json", produces = "application/json")
+ public ResponseEntity createUser(@RequestBody User user) {
+ var status = db.createUser(user);
+ if (status == 0) {
+ return ResponseEntity.ok("{ \"key\": \"aoeuhtns\" }");
+ //var sessionKey = db.generateSessionKey(user);
+ } else {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": " + status + " }");
+ }
+ }
+
+ /**
+ * DO NOT RETURN CODE DIRECTLY!
+ *
+ * User should only ever recieve -1, 0, or 1. The specific authentication error should be hidden.
+ *
+ * Acceptable codes
+ * 0 => Success
+ * 1 => Unregistered email address
+ * 2 => Incorrect password
+ *
+ * -1 => Unknown error
+ */
+ @RequestMapping(value = "/login", method = POST, consumes = "application/json", produces = "application/json")
+ public ResponseEntity login(@RequestBody User user) {
+ var status = db.login(user);
+ if (status == 0) {
+ return ResponseEntity.ok("{ \"key\": \"aoeuhtns\" }");
+ } else if (status > 0) {
+ // Hardcoded 1 response code
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": 1 }");
+ } else {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": " + status + " }");
+ }
+ }
}
diff --git a/backend/src/main/java/achievements/data/User.java b/backend/src/main/java/achievements/data/User.java
new file mode 100644
index 0000000..b0e086b
--- /dev/null
+++ b/backend/src/main/java/achievements/data/User.java
@@ -0,0 +1,43 @@
+package achievements.data;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class User {
+
+ @JsonProperty("email")
+ public String email;
+ @JsonProperty("username")
+ public String username;
+ @JsonProperty("password")
+ public String password;
+
+ public User(String email, String username, String password) {
+ this.email = email;
+ this.username = username;
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/backend/src/main/java/achievements/misc/Password.java b/backend/src/main/java/achievements/misc/Password.java
new file mode 100644
index 0000000..3707221
--- /dev/null
+++ b/backend/src/main/java/achievements/misc/Password.java
@@ -0,0 +1,95 @@
+package achievements.misc;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+
+public class Password {
+
+ private static final Random RANDOM = new SecureRandom();
+
+ public final String salt;
+ public final String hash;
+
+ private Password(String salt, String hash) {
+ this.salt = salt;
+ this.hash = hash;
+ }
+
+ public static Password generate(String password) {
+ // Generate the salt
+ var salt = new byte[16]; // 128 bits
+ RANDOM.nextBytes(salt);
+
+ return new Password(
+ encode(salt),
+ encode(hash(salt, password.getBytes()))
+ );
+ }
+
+ public static boolean validate(String salt, String password, String hash) {
+ System.out.println(salt + ", " + password);
+ var srcHash = hash(decode(salt), password.getBytes());
+ var targetHash = decode(hash);
+ for (int i = 0; i < srcHash.length; ++i) {
+ if (srcHash[i] != targetHash[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static byte[] hash(byte[] salt, byte[] password) {
+ try {
+ var concat = new byte[salt.length + password.length];
+ int i = 0;
+ for (; i < salt.length; ++i) {
+ concat[i] = salt[i];
+ }
+ for (int j = 0; j < password.length; ++j) {
+ concat[i + j] = password[j];
+ }
+
+ var md = MessageDigest.getInstance("SHA-256");
+ return md.digest(concat);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private static String encode(byte[] bytes) {
+ var chars = new char[bytes.length << 1];
+ for (int i = 0; i < bytes.length; ++i) {
+ chars[(i << 1) ] = toHex(bytes[i] >> 0);
+ chars[(i << 1) + 1] = toHex(bytes[i] >> 4);
+ }
+ return new String(chars);
+ }
+
+ private static byte[] decode(String data) {
+ var decoded = new byte[data.length() >> 1];
+ for (int i = 0; i < data.length(); i += 2) {
+ int currentByte =
+ (fromHex(data.charAt(i )) ) |
+ (fromHex(data.charAt(i + 1)) << 4);
+ decoded[i >> 1] = (byte) (currentByte & 0xFF);
+ }
+ return decoded;
+ }
+
+ private static char toHex(int halfByte) {
+ halfByte = halfByte & 0xF;
+ if (0 <= halfByte && halfByte <= 9 ) return (char) (halfByte + '0' );
+ if (10 <= halfByte && halfByte <= 15) return (char) (halfByte + 'a' - 10);
+ return '0';
+ }
+
+ private static int fromHex(char c) {
+ if ('0' <= c && c <= '9') return c - '0';
+ if ('A' <= c && c <= 'F') return c - 'A' + 10;
+ if ('a' <= c && c <= 'f') return c - 'a' + 10;
+ return 0;
+ }
+}
diff --git a/backend/src/main/java/achievements/services/DbService.java b/backend/src/main/java/achievements/services/DbService.java
index 20eca43..d00df35 100644
--- a/backend/src/main/java/achievements/services/DbService.java
+++ b/backend/src/main/java/achievements/services/DbService.java
@@ -2,6 +2,8 @@ package achievements.services;
import achievements.data.Achievements;
import achievements.data.Games;
+import achievements.data.User;
+import achievements.misc.Password;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -84,4 +86,49 @@ public class DbService {
return null;
}
}
+
+ public int createUser(User user) {
+ try {
+ var statement = db.prepareCall("{? = call CreateUser(?, ?, ?, ?)}");
+ statement.registerOutParameter(1, Types.INTEGER);
+ statement.setString(2, user.getEmail());
+ statement.setString(3, user.getUsername());
+
+ var password = Password.generate(user.getPassword());
+ statement.setString(4, password.salt);
+ statement.setString(5, password.hash);
+
+ statement.execute();
+ var code = statement.getInt(1);
+ statement.close();
+
+ return code;
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
+ public int login(User user) {
+ try {
+ var statement = db.prepareStatement("SELECT Salt, Password FROM [dbo].[User] WHERE Email = ?");
+ statement.setString(1, user.email);
+
+ var result = statement.executeQuery();
+ if (result.next()) {
+ var salt = result.getString("Salt");
+ var hash = result.getString("Password");
+ if (Password.validate(salt, user.getPassword(), hash)) {
+ return 0;
+ } else {
+ return 2;
+ }
+ } else {
+ return 1;
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
}
diff --git a/frontend/static_server.js b/frontend/static_server.js
index 75509c5..bcf5afe 100644
--- a/frontend/static_server.js
+++ b/frontend/static_server.js
@@ -1,7 +1,5 @@
-const express = require('express');
-const morgan = require('morgan' );
-const fs = require('fs' );
-const https = require('https' );
+const express = require('express');
+const morgan = require('morgan' );
const config = require('./config.js').load(process.argv[2]);
diff --git a/frontend/webpage/index.html b/frontend/webpage/index.html
index f5ccc0d..0ca9edc 100644
--- a/frontend/webpage/index.html
+++ b/frontend/webpage/index.html
@@ -4,6 +4,8 @@
Achievements Project
+
+
diff --git a/frontend/webpage/login.html b/frontend/webpage/login.html
new file mode 100644
index 0000000..64fd570
--- /dev/null
+++ b/frontend/webpage/login.html
@@ -0,0 +1,41 @@
+
+
+
+
+ Achievements Project | Login
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/webpage/scripts/login.js b/frontend/webpage/scripts/login.js
new file mode 100644
index 0000000..7096ca8
--- /dev/null
+++ b/frontend/webpage/scripts/login.js
@@ -0,0 +1,122 @@
+window.addEventListener("load", (loadEvent) => {
+ const fields = {
+ email: document.querySelector("#email" ),
+ username: document.querySelector("#username"),
+ password: document.querySelector("#password"),
+ confirm: document.querySelector("#confirm" )
+ };
+
+ const createUser = document.querySelector("#create-user-button");
+ const login = document.querySelector("#login-button");
+
+ const header = document.querySelector("#login-header-text");
+ const error = document.querySelector("#error-message");
+
+ const raiseError = (errorFields, message) => {
+ for (const key in fields) {
+ if (errorFields.includes(key)) {
+ fields[key].classList.add("error");
+ } else {
+ fields[key].classList.remove("error");
+ }
+ }
+
+ error.style.display = "block";
+ error.textContent = message;
+ }
+
+ let frozen = false;
+
+ const switchToCreateAction = (clickEvent) => {
+ if (!frozen) {
+ fields.username.style.display = "block";
+ fields.confirm.style.display = "block";
+ login.style.display = "none";
+ header.textContent = "Create User";
+
+ createUser.removeEventListener("click", switchToCreateAction);
+ createUser.addEventListener("click", createUserAction);
+ }
+ };
+ const createUserAction = (clickEvent) => {
+ if (!frozen) {
+ if (fields.email.value === '') {
+ raiseError([ "email" ], "Email cannot be empty");
+ } else if (fields.username.value === '') {
+ raiseError([ "username" ], "Username cannot be empty");
+ } else if (fields.password.value !== fields.confirm.value) {
+ raiseError([ "password", "confirm" ], "Password fields did not match");
+ } else if (fields.password.value === '') {
+ raiseError([ "password", "confirm" ], "Password cannot be empty");
+ } else {
+ frozen = true;
+ fetch('https://localhost:4730/create_user', {
+ method: 'POST',
+ mode: 'cors',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ email: fields.email.value, username: fields.username.value, password: fields.password.value })
+ })
+ .then(async response => ({ status: response.status, data: await response.json() }))
+ .then(response => {
+ const data = response.data;
+ if (response.status === 200) {
+ window.sessionStorage.setItem('sessionKey', data.key);
+ window.location.href = "/";
+ } else if (response.status === 500) {
+ raiseError([], "Internal server error :(");
+ } else {
+ if (data.code === 1) {
+ raiseError([ "email" ], "A user with that email is already registered");
+ fields.email.value = '';
+ }
+ }
+ })
+ .catch(error => {
+ console.error(error);
+ raiseError([], "Server error :(");
+ }).then(() => frozen = false);
+ }
+ }
+ };
+ createUser.addEventListener("click", switchToCreateAction);
+
+ const loginAction = (clickEvent) => {
+ if (!frozen) {
+ if (fields.email.value === '') {
+ raiseError([ "email" ], "Email cannot be empty");
+ } else if (fields.password.value === '') {
+ raiseError([ "password" ], "Password cannot be empty");
+ } else {
+ frozen = true;
+ fetch('https://localhost:4730/login', {
+ method: 'POST',
+ mode: 'cors',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ email: fields.email.value, password: fields.password.value })
+ })
+ .then(async response => ({ status: response.status, data: await response.json() }))
+ .then(response => {
+ const data = response.data;
+ if (response.status === 200) {
+ window.sessionStorage.setItem('sessionKey', data.key);
+ window.location.href = "/";
+ } else if (response.status === 500) {
+ raiseError([], "Internal server error :(");
+ } else {
+ raiseError([ "email", "password" ], "Email or password is incorrect");
+ fields.password.value = '';
+ }
+ })
+ .catch(error => {
+ console.error(error);
+ raiseError([], "Unknown error :(");
+ }).then(() => frozen = false);
+ }
+ }
+ };
+ login.addEventListener("click", loginAction);
+});
\ No newline at end of file
diff --git a/frontend/webpage/scripts/template.js b/frontend/webpage/scripts/template.js
index 7e302ea..ecc1a88 100644
--- a/frontend/webpage/scripts/template.js
+++ b/frontend/webpage/scripts/template.js
@@ -1,142 +1,141 @@
var template = template || {};
-template.type = {};
-template.type._entryMap = new Map();
-template.type.register = (type, callback) => {
- if (typeof type !== 'string') {
- console.error(`'type' must be a string, recieved: `, type);
- } else {
- const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/;
- const result = type.match(TYPE_REGEX);
- if (result === null) {
- console.error(`'${type}' is not a valid type id`);
- } else {
- if (result[2] === undefined) {
- result[2] = 0;
- } else {
- result[2] = result[2].split(/\s*,\s*/).length;
- }
- const completeType = result[1] + ':' + result[2];
- if (template.type._entryMap.get(completeType) === undefined) {
- template.type._entryMap.set(completeType, async function() {
- await callback.apply(null, Array.from(arguments));
- });
- } else {
- console.error(`${type} is already a registered template!`);
- }
- }
- }
-};
-
-// Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
-function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string
-}
-
-/* Intrinsic Templates */
-
-// Basic - Simple search and replace
-template.type.register('Basic', (element, map) => {
- let html = element.innerHTML;
- function applyObject(object, path) {
- for (const key in object) {
- const regexKey = escapeRegExp(path + key);
- html = html.replace(new RegExp(`(?:(? {
- return fetch(`templates/${name}.html.template`, {
- method: 'GET',
- mode: 'no-cors',
- headers: {
- 'Content-Type': 'text/plain'
- }
- }).then(response => response.text().then((data) => {
- element.outerHTML = data;
- })).catch(error => {
- console.error(`failed to retrieve template '${name}': `, error);
- });
-});
-
-// List - Iterate over list and emit copy of child for each iteration
-template.type.register('List>', async (element, subtype, arrayMap) => {
- let cumulative = '';
- const temp = document.createElement('template');
- for (const obj of arrayMap) {
- temp.innerHTML = ``;
- const child = temp.content.children[0];
- child.innerHTML = element.innerHTML;
- const callback = template.type._entryMap.get(subtype.type);
- if (callback === undefined) {
- cumulative = '';
- console.error(`'${subtype.type}' is not a registered template type`);
- } else {
- await callback.apply(null, [ child, obj ]);
- }
- cumulative = cumulative + temp.innerHTML.trim();
- }
- element.outerHTML = cumulative;
-});
-
-template._entryMap = new Map();
-template.apply = function(pattern, promise) {
- if (typeof pattern !== 'string') {
- console.error('pattern must be a string, received: ', pattern);
- } else {
- return new template.apply.applicators(pattern);
- }
-};
-template.apply.applicators = class {
- constructor(pattern) {
- this._pattern = pattern;
- }
-
- _apply(asyncArgs) {
- template._entryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs);
- }
-
- values(...args) {
- this._apply(async () => Array.from(args));
- }
-
- promise(promise) {
- let args = null;
- promise = promise.then(data => args = [ data ]);
- this._apply(async () => args || promise);
- }
-
- fetch(dataProcessor, url, options) {
- if (typeof dataProcessor === 'string') {
- const path = dataProcessor;
- dataProcessor = data => {
- for (const id of path.split(/\./)) {
- data = data[id];
- if (data === undefined) {
- throw `invalid path '${path}'`;
- }
- }
-
- return data;
- };
- };
- this.promise(
- fetch(url, options || { method: 'GET', mode: 'cors' })
- .then(response => response.json())
- .then(data => dataProcessor(data))
- );
- }
-};
-
(() => {
+ templateTypeEntryMap = new Map();
+ template.register = (type, callback) => {
+ if (typeof type !== 'string') {
+ console.error(`'type' must be a string, recieved: `, type);
+ } else {
+ const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/;
+ const result = type.match(TYPE_REGEX);
+ if (result === null) {
+ console.error(`'${type}' is not a valid type id`);
+ } else {
+ if (result[2] === undefined) {
+ result[2] = 0;
+ } else {
+ result[2] = result[2].split(/\s*,\s*/).length;
+ }
+ const completeType = result[1] + ':' + result[2];
+ if (templateTypeEntryMap.get(completeType) === undefined) {
+ templateTypeEntryMap.set(completeType, async function() {
+ await callback.apply(null, Array.from(arguments));
+ });
+ } else {
+ console.error(`${type} is already a registered template!`);
+ }
+ }
+ }
+ };
+
+ // Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
+ function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string
+ }
+
+ /* Intrinsic Templates */
+
+ // Basic - Simple search and replace
+ template.register('Basic', (element, map) => {
+ let html = element.innerHTML;
+ function applyObject(object, path) {
+ for (const key in object) {
+ const regexKey = escapeRegExp(path + key);
+ html = html.replace(new RegExp(`(?:(? {
+ return fetch(`templates/${name}.html.template`, {
+ method: 'GET',
+ mode: 'no-cors',
+ headers: {
+ 'Content-Type': 'text/plain'
+ }
+ }).then(response => response.text().then((data) => {
+ element.outerHTML = data;
+ })).catch(error => {
+ console.error(`failed to retrieve template '${name}': `, error);
+ });
+ });
+
+ // List - Iterate over list and emit copy of child for each iteration
+ template.register('List>', async (element, subtype, arrayMap) => {
+ let cumulative = '';
+ const temp = document.createElement('template');
+ for (const obj of arrayMap) {
+ temp.innerHTML = ``;
+ const child = temp.content.children[0];
+ child.innerHTML = element.innerHTML;
+ const callback = templateTypeEntryMap.get(subtype.type);
+ if (callback === undefined) {
+ cumulative = '';
+ console.error(`'${subtype.type}' is not a registered template`);
+ } else {
+ await callback.apply(null, [ child, obj ]);
+ }
+ cumulative = cumulative + temp.innerHTML.trim();
+ }
+ element.outerHTML = cumulative;
+ });
+
+ templateEntryMap = new Map();
+ template.apply = function(pattern, promise) {
+ if (typeof pattern !== 'string') {
+ console.error('pattern must be a string, received: ', pattern);
+ } else {
+ return new template.apply.applicators(pattern);
+ }
+ };
+ template.apply.applicators = class {
+ constructor(pattern) {
+ this._pattern = pattern;
+ }
+
+ _apply(asyncArgs) {
+ templateEntryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs);
+ }
+
+ values(...args) {
+ this._apply(async () => Array.from(args));
+ }
+
+ promise(promise) {
+ let args = null;
+ promise = promise.then(data => args = [ data ]);
+ this._apply(async () => args || promise);
+ }
+
+ fetch(dataProcessor, url, options) {
+ if (typeof dataProcessor === 'string') {
+ const path = dataProcessor;
+ dataProcessor = data => {
+ for (const id of path.split(/\./)) {
+ data = data[id];
+ if (data === undefined) {
+ throw `invalid path '${path}'`;
+ }
+ }
+
+ return data;
+ };
+ };
+ this.promise(
+ fetch(url, options || { method: 'GET', mode: 'cors' })
+ .then(response => response.json())
+ .then(data => dataProcessor(data))
+ );
+ }
+ };
+
const parseType = (type) => {
let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/);
let id = result[1];
@@ -162,12 +161,12 @@ template.apply.applicators = class {
let promises = [];
let parents = new Set();
for (const child of children) {
- for (const [pattern, argsCallback] of template._entryMap) {
+ for (const [pattern, argsCallback] of templateEntryMap) {
await argsCallback().then(args => {
if (pattern.test(child.id)) {
- const callback = template.type._entryMap.get(child.typeCapture.type);
+ const callback = templateTypeEntryMap.get(child.typeCapture.type);
if (typeof callback !== 'function') {
- console.error(`'${child.typeCapture.type}' is not a registered template type`);
+ console.error(`'${child.typeCapture.type}' is not a registered template`);
} else {
let params = Array.from(args)
for (const subtype of child.typeCapture.params) {
diff --git a/frontend/webpage/styles/common.css b/frontend/webpage/styles/common.css
new file mode 100644
index 0000000..6fe8b0d
--- /dev/null
+++ b/frontend/webpage/styles/common.css
@@ -0,0 +1,378 @@
+html, body {
+ background-color: var(--background-dark);
+
+ margin: 0;
+ border: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+
+ font-family: sans-serif;
+}
+
+#navbar {
+ z-index: 1;
+
+ position: fixed;
+
+ background-color: var(--accent-value2);
+ color: var(--foreground);
+
+ width: 100%;
+ min-height: 76px;
+ height: 5%;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5);
+}
+
+.navbar-section {
+ width: max-content;
+ height: 100%;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+}
+
+.navbar-item {
+ box-sizing: border-box;
+ padding: 0px 20px;
+
+ width: max-content;
+ height: 100%;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+ font-size: 24px;
+
+ user-select: none;
+
+ transition-property: background-color;
+ transition-duration: 0.15s;
+
+ position: relative;
+}
+
+.navbar-item:hover {
+ background-color: var(--accent-value3);
+}
+
+.ap-button {
+ box-sizing: border-box;
+
+ padding: 12px 16px;
+
+ color: var(--foreground);
+ background-color: var(--accent-value2);
+
+ font-size: 18px;
+ text-align: center;
+
+ border-radius: 4px;
+
+ cursor: default;
+
+ transition-property: background-color;
+ transition-duration: 0.15s;
+}
+
+.ap-button:hover {
+ background-color: var(--accent-value3);
+}
+
+.ap-button:active {
+ background-color: var(--accent-value1);
+}
+
+#content-body {
+ position: relative;
+
+ top: max(76px, 5%);
+
+ width: 100%;
+ height: calc(100% - max(76px, 5%));
+
+ overflow-y: auto;
+
+ display: flex;
+ justify-content: center;
+}
+
+.page {
+ z-index: 0;
+
+ box-sizing: border-box;
+
+ padding: 0px 64px;
+
+ width: 100%;
+ height: max-content;
+ min-height: 100%;
+
+ background-color: var(--background);
+ box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5);
+
+ display: none;
+}
+
+.page-header {
+ box-sizing: border-box;
+
+ padding: 64px 0px;
+
+ width: 100%;
+ height: max-content;
+}
+
+.page-header-text,
+.page-subheader-text {
+ width: max-content;
+
+ margin: 0;
+ margin-bottom: 0.25em;
+
+ color: var(--foreground);
+
+ cursor: default;
+}
+
+.page-header-text.link,
+.page-subheader-text.link {
+ transition-property: color;
+ transition-duration: 0.15s;
+}
+
+.page-header-text.link:hover,
+.page-subheader-text.link:hover {
+ color: var(--accent-value4);
+}
+
+.page-header-text {
+ font-size: 64px;
+}
+
+.page-subheader-text {
+ font-size: 48px;
+}
+
+.page-header-separator,
+.page-subheader-separator {
+ width: 100%;
+ height: 3px;
+
+ background-color: var(--accent-value3);
+}
+
+.list-page-search {
+ box-sizing: border-box;
+
+ padding: 0px 64px 32px;
+
+ width: 100%;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+}
+
+.list-page-search > label,
+.list-page-search > input {
+ box-sizing: border-box;
+ padding: 12px 20px;
+
+ color: var(--foreground);
+
+ font-size: 24px;
+}
+
+.list-page-search > label {
+ background-color: var(--accent-value2);
+
+ border-radius: 8px 0px 0px 8px;
+}
+
+.list-page-search > label:hover {
+ background-color: var(--accent-value3);
+}
+
+.list-page-search > label:active {
+ background-color: var(--accent-value1);
+
+ transition-property: background-color;
+ transition-duration: 0.15s;
+}
+
+.list-page-search > input {
+ background-color: var(--distinction);
+
+ border: 0;
+ border-radius: 0px 8px 8px 0px;
+
+ flex-grow: 1;
+
+ outline: none;
+
+ transition-property: background-color, color;
+ transition-duration: 0.075s;
+}
+
+.list-page-search > input:focus {
+ background-color: var(--foreground);
+
+ color: var(--background);
+}
+
+.list-page-partitions {
+ box-sizing: border-box;
+
+ padding: 32px 64px;
+
+ width: 100%;
+ height: max-content;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.list-page-filter-partition {
+ width: 20%;
+ max-width: 640px;
+}
+
+.list-page-filter {
+ margin-top: 16px;
+
+ box-sizing: border-box;
+
+ width: 100%;
+ height: max-content;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+}
+
+.list-page-filter-checkbox {
+ width: 32px;
+ height: 32px;
+
+ background-color: var(--background);
+
+ border: 3px solid var(--distinction);
+ border-radius: 8px;
+
+ transition-property: background-color, border-color;
+ transition-duration: 0.15s;
+}
+
+.list-page-filter:hover > .list-page-filter-checkbox {
+ background-color: var(--background);
+ border-color: var(--selected-accent1);
+}
+
+.list-page-filter.selected > .list-page-filter-checkbox {
+ background-color: var(--selected-accent1);
+ border-color: var(--selected-accent1);
+}
+
+.list-page-filter.selected:hover > .list-page-filter-checkbox {
+ background-color: var(--selected-accent0);
+ border-color: var(--selected-accent1);
+}
+
+.list-page-filter-name {
+ margin: 0;
+ padding: 16px;
+
+ color: var(--foreground);
+
+ font-size: 24px;
+
+ user-select: none;
+}
+
+.list-page-list-partition {
+ box-sizing: border-box;
+
+ padding-left: 64px;
+
+ flex-grow: 1;
+}
+
+.list-page-list {
+ border-radius: 8px;
+
+ overflow: hidden;
+}
+
+.list-page-header {
+ width: 100%;
+ height: 64px;
+
+ background-color: var(--accent-value2);
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+ color: var(--foreground);
+ font-size: 24px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.list-page-entry {
+ width: 100%;
+ height: 64px;
+
+ background-color: var(--distinction);
+
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+ color: var(--foreground);
+ font-size: 24px;
+}
+
+.list-page-entry-icon {
+ width: 64px;
+ height: 64px;
+
+ flex-grow: 0;
+}
+
+.list-page-entry-text {
+ box-sizing: border-box;
+
+ margin: 0;
+ padding: 0 12px;
+ width: 0;
+ height: 64px;
+ line-height: 64px;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ border-top: 1px solid var(--background);
+}
+
+.list-page-header > .list-page-entry-text {
+ border: 0;
+}
diff --git a/frontend/webpage/styles/index.css b/frontend/webpage/styles/index.css
index a9fe898..5975fc8 100644
--- a/frontend/webpage/styles/index.css
+++ b/frontend/webpage/styles/index.css
@@ -1,25 +1,3 @@
-:root {
- --background-dark: #111115;
- --background: #22222A;
- --foreground: #EEEEEE;
- --distinction: #44444F;
-
- --accent-value0: #500000;
- --accent-value1: #800000;
- --accent-value2: #A00000;
- --accent-value3: #D02020;
- --accent-value4: #FA7575;
-
- --selected-accent0: #0066CC;
- --selected-accent1: #3388FF;
-
- --navbar-background: var(--accent-value2);
- --navbar-hover-background: var(--accent-value3);
- --navbar-foreground: #EEEEEE;
-
- --header-color: var(--accent-value3);
-}
-
html, body {
background-color: var(--background-dark);
@@ -32,394 +10,27 @@ html, body {
font-family: sans-serif;
}
-#navbar {
- z-index: 1;
-
- position: fixed;
-
- background-color: var(--navbar-background);
- color: var(--navbar-foreground);
-
- width: 100%;
- min-height: 76px;
- height: 5%;
-
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-
- box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5);
-}
-
-.navbar-section {
- width: max-content;
- height: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
-}
-
-.navbar-item {
- box-sizing: border-box;
- padding: 0px 20px;
-
- width: max-content;
- height: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
-
- font-size: 24px;
-
- user-select: none;
-
- transition-property: background-color;
- transition-duration: 0.15s;
-
- position: relative;
-}
-
-.navbar-item:hover {
- background-color: var(--navbar-hover-background);
-}
-
-#content-body {
- position: relative;
-
- top: max(76px, 5%);
-
- width: 100%;
- height: calc(100% - max(76px, 5%));
-
- overflow-y: auto;
-
- display: flex;
- justify-content: center;
-}
-
-.page {
- z-index: 0;
-
- box-sizing: border-box;
-
- padding: 0px 64px;
-
- width: 100%;
- height: 100%;
-
- background-color: var(--background);
- box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5);
-
- overflow-y: auto;
-
- display: none;
-}
-
-.page-header {
- box-sizing: border-box;
-
- padding: 64px 0px;
-
- width: 100%;
- height: max-content;
-}
-
-.page-header-text {
- width: max-content;
-
- margin: 0;
- margin-bottom: 0.25em;
-
- color: var(--header-color);
-
- font-size: 64px;
-}
-
-.page-header-separator {
- width: 100%;
- height: 3px;
-
- background-color: var(--foreground);
-}
-
-.page-subheader-text {
- width: max-content;
-
- margin: 0;
- margin-bottom: 0.25em;
-
- color: var(--header-color);
-
- font-size: 48px;
-
- user-select: none;
-}
-
-.page-subheader-separator {
- width: 100%;
- height: 3px;
-
- background-color: var(--foreground);
-
- transition-property: color;
- transition-duration: 0.15s;
-}
-
-.list-page-search {
- box-sizing: border-box;
-
- padding: 32px 64px;
-
- width: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
-}
-
-.list-page-search > label {
- box-sizing: border-box;
- padding: 16px 24px;
-
- background-color: var(--accent-value2);
-
- color: var(--foreground);
-
- font-size: 32px;
-
- border-radius: 8px 0px 0px 8px;
-
- transition-property: background-color;
- transition-duration: 0.15s;
-}
-
-.list-page-search > label:hover {
- background-color: var(--accent-value3);
-}
-
-.list-page-search > label:active {
- background-color: var(--accent-value1);
-}
-
-.list-page-search > input {
- box-sizing: border-box;
- padding: 16px 24px;
-
- background-color: var(--distinction);
-
- color: var(--foreground);
-
- font-size: 32px;
-
- border: 0;
- border-radius: 0px 8px 8px 0px;
-
- flex-grow: 1;
-
- outline: none;
-
- transition-property: background-color, color;
- transition-duration: 0.075s;
-}
-
-.list-page-search > input:focus {
- background-color: var(--foreground);
- color: var(--background);
-}
-
-.list-page-partitions {
- box-sizing: border-box;
-
- padding: 32px 64px;
-
- width: 100%;
- height: max-content;
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: flex-start;
-}
-
-.list-page-filter-partition {
- width: 20%;
- max-width: 640px;
-}
-
-.list-page-filter {
- margin-top: 16px;
-
- box-sizing: border-box;
-
- width: 100%;
- height: max-content;
-
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
-}
-
-.list-page-filter-checkbox {
- width: 32px;
- height: 32px;
-
- background-color: var(--background);
-
- border: 3px solid var(--distinction);
- border-radius: 8px;
-
- transition-property: background-color, border-color;
- transition-duration: 0.15s;
-}
-
-.list-page-filter:hover > .list-page-filter-checkbox {
- background-color: var(--background);
- border-color: var(--selected-accent1);
-}
-
-.list-page-filter.selected > .list-page-filter-checkbox {
- background-color: var(--selected-accent1);
- border-color: var(--selected-accent1);
-}
-
-.list-page-filter.selected:hover > .list-page-filter-checkbox {
- background-color: var(--selected-accent0);
- border-color: var(--selected-accent1);
-}
-
-.list-page-filter-name {
- margin: 0;
- padding: 16px;
-
- color: var(--foreground);
-
- font-size: 24px;
-
- user-select: none;
-}
-
-.list-page-list-partition {
- box-sizing: border-box;
-
- padding-left: 64px;
-
- flex-grow: 1;
-}
-
-.list-page-list {
- border-radius: 8px;
-
- overflow: hidden;
- overflow-y: auto;
-}
-
-.list-page-list-header {
- width: 100%;
- height: 64px;
-
- background-color: var(--accent-value2);
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
-
- color: var(--foreground);
- font-size: 24px;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.list-page-list-entry {
- width: 100%;
- height: 64px;
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
-
- color: var(--foreground);
- font-size: 24px;
-
- border-bottom: 1px solid var(--distinction);
-}
-
#games-page {
max-width: 1920px;
}
-.game-list-page-entry-icon {
- width: 64px;
- height: 64px;
-
- flex-grow: 0;
-}
-
-.game-list-page-entry-name {
- box-sizing: border-box;
-
- margin: 0;
- padding: 0 12px;
- width: 0;
-
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-
+.list-page-entry-text.game-name {
flex-grow: 1;
- flex-basis: 0px;
}
-.game-list-page-entry-description {
- box-sizing: border-box;
-
- margin: 0;
- padding: 0 12px;
- width: 0;
-
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-
+.list-page-entry-text.game-description {
flex-grow: 2;
- flex-basis: 0px;
}
#achievements-page {
max-width: 1920px;
}
-.achievement-list-page-entry-icon {
- width: 64px;
- height: 64px;
-
- flex-grow: 0;
-}
-
-.achievement-list-page-entry-name {
- box-sizing: border-box;
-
- margin: 0;
- padding: 0 12px;
- width: 0;
-
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-
+.list-page-entry-text.achievement-name {
flex-grow: 4;
- flex-basis: 0px;
}
-.achievement-list-page-entry-description {
+.list-page-entry-text.achievement-description {
box-sizing: border-box;
margin: 0;
@@ -431,22 +42,10 @@ html, body {
white-space: nowrap;
flex-grow: 8;
- flex-basis: 0px;
}
-.achievement-list-page-entry-stages {
- box-sizing: border-box;
-
- margin: 0;
- padding: 0 12px;
- width: 0;
-
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-
+.list-page-entry-text.achievement-stages {
flex-grow: 1;
- flex-basis: 0px;
}
#profile-page {
@@ -482,6 +81,8 @@ html, body {
#profile-info-pfp {
width: 100%;
max-width: 640px;
+
+ margin-bottom: 1.25em;
}
#profile-info-name {
@@ -491,7 +92,78 @@ html, body {
font-size: 42px;
- color: var(--header-color);
+ color: var(--foreground);
+}
+
+.profile-list {
+ margin-top: 1.25em;
+
+ width: 100%;
+ height: max-content;
+
+ border-radius: 8px;
+
+ overflow: hidden;
+}
+
+.profile-entry {
+ overflow: hidden;
+
+ height: 64px;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ background-color: var(--distinction);
+
+ border-bottom: 1px solid var(--background);
+}
+
+.profile-entry.accented {
+ border-bottom: 1px solid var(--accent-value0);
+
+ background-color: var(--accent-value1);
+}
+
+.profile-entry-left {
+ height: 100%;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+}
+
+.profile-entry-right {
+ height: 100%;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+}
+
+.profile-entry-icon {
+ width: 64px;
+}
+
+.profile-entry-text {
+ margin: 0;
+ padding: 0px 16px;
+
+ color: var(--foreground);
+
+ font-size: 24px;
+}
+
+.profile-entry-text.accented {
+ display: none;
+}
+
+.profile-entry.accented .profile-entry-text.accented {
+ display: block;
}
#profile-platforms {
@@ -504,74 +176,6 @@ html, body {
flex-grow: 1;
}
-.profile-platform-entry {
- overflow: hidden;
-
- margin-top: 16px;
-
- height: 64px;
-
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-
- border: 3px solid var(--distinction);
- border-radius: 12px;
-}
-
-.profile-platform-entry.connected {
- border: 3px solid var(--accent-value3);
-
- background-color: var(--accent-value2);
-}
-
-.profile-platform-entry-left {
- height: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
-}
-
-.profile-platform-entry-right {
- height: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
- align-items: center;
-}
-
-.profile-platform-icon {
- width: 64px;
-}
-
-.profile-platform-name {
- margin: 0;
- padding: 0px 16px;
-
- color: var(--foreground);
-
- font-size: 24px;
-}
-
-.profile-platform-connected {
- display: none;
-
- margin: 0;
- padding: 0px 16px;
-
- color: var(--foreground);
-
- font-size: 24px;
-}
-
-.profile-platform-entry.connected .profile-platform-connected {
- display: block;
-}
-
#profile-section-2 {
box-sizing: border-box;
@@ -586,122 +190,16 @@ html, body {
#profile-games {
box-sizing: border-box;
- padding: 0px 64px;
+ padding: 0px 64px 64px;
width: 50%;
height: max-content;
}
-#profile-games > .page-subheader-text:hover {
- color: var(--accent-value4);
-}
-
-.profile-game-entry {
- overflow: hidden;
-
- margin-top: 16px;
-
- height: 64px;
-
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
-
- border: 3px solid var(--distinction);
- border-radius: 12px;
-}
-
-.profile-game-entry-icon {
- height: 64px;
-}
-
-.profile-game-entry-name {
- margin: 0;
- padding: 0px 16px;
-
- color: var(--foreground);
-
- font-size: 24px;
-}
-
#profile-achievements {
box-sizing: border-box;
- padding: 0px 64px;
+ padding: 0px 64px 64px;
width: 50%;
height: max-content;
-}
-
-#profile-achievements > .page-subheader-text:hover {
- color: var(--accent-value4);
-}
-
-.profile-achievement-entry {
- overflow: hidden;
-
- margin-top: 16px;
-
- height: 64px;
-
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-
- border: 3px solid var(--distinction);
- border-radius: 12px;
-}
-
-.profile-achievement-entry.completed {
- border: 3px solid var(--accent-value3);
-
- background-color: var(--accent-value2);
-}
-
-.profile-achievement-entry-left {
- height: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
-}
-
-.profile-achievement-entry-right {
- height: 100%;
-
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
- align-items: center;
-}
-
-.profile-achievement-entry-icon {
- height: 64px;
-}
-
-.profile-achievement-entry-name {
- margin: 0;
- padding: 0px 16px;
-
- color: var(--foreground);
-
- font-size: 24px;
-}
-
-.profile-achievement-completed {
- display: none;
-
- margin: 0;
- padding: 0px 16px;
-
- color: var(--foreground);
-
- font-size: 24px;
-}
-
-.profile-achievement-entry.completed .profile-achievement-completed {
- display: block;
-}
-
+}
\ No newline at end of file
diff --git a/frontend/webpage/styles/login.css b/frontend/webpage/styles/login.css
new file mode 100644
index 0000000..707bc3a
--- /dev/null
+++ b/frontend/webpage/styles/login.css
@@ -0,0 +1,101 @@
+:root {
+ --form-spacing: 48px;
+
+ --element-spacing: 12px;
+
+ --error: #FA7575;
+}
+
+#login-page {
+ display: block;
+
+ max-width: 1280px;
+}
+
+#login-header {
+ box-sizing: border-box;
+
+ padding: 0 calc(25% - 64px);
+
+ width: 100%;
+ height: max-content;
+}
+
+#login-form {
+ box-sizing: border-box;
+
+ margin: 24px calc(25% - 64px) 0;
+ padding: 24px 0;
+
+ height: max-content;
+
+ background-color: var(--distinction);
+
+ border-radius: 8px;
+}
+
+#error-message {
+ display: none;
+
+ box-sizing: border-box;
+
+ margin: 0 var(--form-spacing) var(--element-spacing);
+ width: calc(100% - (var(--form-spacing) * 2));
+
+ color: var(--error);
+
+ font-size: 20px;
+}
+
+.login-field {
+ box-sizing: border-box;
+
+ margin: 0 var(--form-spacing) var(--element-spacing);
+ border: 0;
+ padding: 8px;
+
+ width: calc(100% - (var(--form-spacing) * 2));
+ height: max-content;
+
+ font-size: 20px;
+
+ border-radius: 4px;
+
+ outline: none;
+}
+
+.login-field.error {
+ background-color: var(--error);
+}
+
+#username,
+#confirm {
+ display: none;
+}
+
+#login-buttons {
+ margin: 0 calc(var(--form-spacing) - (var(--element-spacing) / 2));
+ width: calc(100% - (var(--form-spacing) * 2) + var(--element-spacing));
+ height: max-content;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.ap-button.login {
+ margin: 0 calc(var(--element-spacing) / 2);
+ flex-grow: 1;
+ flex-basis: 0px;
+}
+
+#warning {
+ box-sizing: border-box;
+
+ padding: 0 var(--form-spacing);
+
+ color: var(--foreground);
+
+ font-size: 24px;
+}
\ No newline at end of file
diff --git a/frontend/webpage/styles/theme.css b/frontend/webpage/styles/theme.css
new file mode 100644
index 0000000..b2f059d
--- /dev/null
+++ b/frontend/webpage/styles/theme.css
@@ -0,0 +1,16 @@
+:root {
+ --background-dark: #111115;
+ --background: #22222A;
+ --foreground-dark: #AAAAAA;
+ --foreground: #EEEEEE;
+ --distinction: #44444F;
+
+ --accent-value0: #500000;
+ --accent-value1: #800000;
+ --accent-value2: #A00000;
+ --accent-value3: #D02020;
+ --accent-value4: #FA7575;
+
+ --selected-accent0: #0066CC;
+ --selected-accent1: #3388FF;
+}
\ No newline at end of file
diff --git a/frontend/webpage/templates/achievements_page.html.template b/frontend/webpage/templates/achievements_page.html.template
index b4fb2a8..147ef4d 100644
--- a/frontend/webpage/templates/achievements_page.html.template
+++ b/frontend/webpage/templates/achievements_page.html.template
@@ -1,38 +1,38 @@
-
+