Added user login and account creation

This commit is contained in:
Gnarwhal 2021-02-05 04:59:17 -05:00
parent 9ba8a99e82
commit 5a1dd33dfe
Signed by: Gnarwhal
GPG key ID: 0989A73D8C421174
19 changed files with 1276 additions and 874 deletions

View file

@ -1,5 +1,6 @@
package achievements; package achievements;
import achievements.misc.Password;
import achievements.services.DbConnectionService; import achievements.services.DbConnectionService;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

View file

@ -1,6 +1,7 @@
package achievements.controllers; package achievements.controllers;
import achievements.data.Achievements; import achievements.data.Achievements;
import achievements.data.User;
import achievements.data.Games; import achievements.data.Games;
import achievements.data.InternalError; import achievements.data.InternalError;
import achievements.services.DbService; import achievements.services.DbService;
@ -12,6 +13,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController @RestController
public class Controller { public class Controller {
@ -62,4 +64,47 @@ public class Controller {
return new ResponseEntity("{}", HttpStatus.INTERNAL_SERVER_ERROR); 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 + " }");
}
}
} }

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -2,6 +2,8 @@ package achievements.services;
import achievements.data.Achievements; import achievements.data.Achievements;
import achievements.data.Games; import achievements.data.Games;
import achievements.data.User;
import achievements.misc.Password;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -84,4 +86,49 @@ public class DbService {
return null; 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;
}
} }

View file

@ -1,7 +1,5 @@
const express = require('express'); const express = require('express');
const morgan = require('morgan' ); const morgan = require('morgan' );
const fs = require('fs' );
const https = require('https' );
const config = require('./config.js').load(process.argv[2]); const config = require('./config.js').load(process.argv[2]);

View file

@ -4,6 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Achievements Project</title> <title>Achievements Project</title>
<link rel="stylesheet" href="styles/theme.css" />
<link rel="stylesheet" href="styles/common.css" />
<link rel="stylesheet" href="styles/index.css" /> <link rel="stylesheet" href="styles/index.css" />
</head> </head>
<body> <body>

View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project | Login</title>
<link rel="stylesheet" href="styles/theme.css" />
<link rel="stylesheet" href="styles/common.css" />
<link rel="stylesheet" href="styles/login.css" />
</head>
<body>
<div id="navbar"></div>
<div id="content-body">
<div id="login-page" class="page">
<div class="page-header">
<p class="page-header-text">Achievements Project</p>
<div class="page-header-separator"></div>
</div>
<div id="login-header">
<p id="login-header-text" class="page-subheader-text">Login</p>
<div class="page-subheader-separator"></div>
</div>
<div id="login-form">
<p id="error-message">Egg</p>
<input id="email" class="login-field" type="text" placeholder="Email"></input>
<input id="username" class="login-field" type="text" placeholder="Username"></input>
<input id="password" class="login-field" type="password" placeholder="Password"></input>
<input id="confirm" class="login-field" type="password" placeholder="Confirm your password"></input>
<div id="login-buttons">
<div id="create-user-button" class="ap-button login">Create Account</div>
<div id="login-button" class="ap-button login">Login</div>
</div>
<p id="warning">WARNING! The security of this project is questionable at best. Please refrain from using any truly sensitive data.</p>
</div>
</div>
</div>
<script src="scripts/template.js"></script>
<script src="scripts/login.js"></script>
</body>
</html>

View file

@ -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);
});

View file

@ -1,8 +1,8 @@
var template = template || {}; var template = template || {};
template.type = {}; (() => {
template.type._entryMap = new Map(); templateTypeEntryMap = new Map();
template.type.register = (type, callback) => { template.register = (type, callback) => {
if (typeof type !== 'string') { if (typeof type !== 'string') {
console.error(`'type' must be a string, recieved: `, type); console.error(`'type' must be a string, recieved: `, type);
} else { } else {
@ -17,8 +17,8 @@ template.type.register = (type, callback) => {
result[2] = result[2].split(/\s*,\s*/).length; result[2] = result[2].split(/\s*,\s*/).length;
} }
const completeType = result[1] + ':' + result[2]; const completeType = result[1] + ':' + result[2];
if (template.type._entryMap.get(completeType) === undefined) { if (templateTypeEntryMap.get(completeType) === undefined) {
template.type._entryMap.set(completeType, async function() { templateTypeEntryMap.set(completeType, async function() {
await callback.apply(null, Array.from(arguments)); await callback.apply(null, Array.from(arguments));
}); });
} else { } else {
@ -36,7 +36,7 @@ function escapeRegExp(string) {
/* Intrinsic Templates */ /* Intrinsic Templates */
// Basic - Simple search and replace // Basic - Simple search and replace
template.type.register('Basic', (element, map) => { template.register('Basic', (element, map) => {
let html = element.innerHTML; let html = element.innerHTML;
function applyObject(object, path) { function applyObject(object, path) {
for (const key in object) { for (const key in object) {
@ -53,7 +53,7 @@ template.type.register('Basic', (element, map) => {
}); });
// Extern - Retrieve template from webserver // Extern - Retrieve template from webserver
template.type.register('Extern', (element, name) => { template.register('Extern', (element, name) => {
return fetch(`templates/${name}.html.template`, { return fetch(`templates/${name}.html.template`, {
method: 'GET', method: 'GET',
mode: 'no-cors', mode: 'no-cors',
@ -68,17 +68,17 @@ template.type.register('Extern', (element, name) => {
}); });
// List - Iterate over list and emit copy of child for each iteration // List - Iterate over list and emit copy of child for each iteration
template.type.register('List<?>', async (element, subtype, arrayMap) => { template.register('List<?>', async (element, subtype, arrayMap) => {
let cumulative = ''; let cumulative = '';
const temp = document.createElement('template'); const temp = document.createElement('template');
for (const obj of arrayMap) { for (const obj of arrayMap) {
temp.innerHTML = `<template></template>`; temp.innerHTML = `<template></template>`;
const child = temp.content.children[0]; const child = temp.content.children[0];
child.innerHTML = element.innerHTML; child.innerHTML = element.innerHTML;
const callback = template.type._entryMap.get(subtype.type); const callback = templateTypeEntryMap.get(subtype.type);
if (callback === undefined) { if (callback === undefined) {
cumulative = ''; cumulative = '';
console.error(`'${subtype.type}' is not a registered template type`); console.error(`'${subtype.type}' is not a registered template`);
} else { } else {
await callback.apply(null, [ child, obj ]); await callback.apply(null, [ child, obj ]);
} }
@ -87,7 +87,7 @@ template.type.register('List<?>', async (element, subtype, arrayMap) => {
element.outerHTML = cumulative; element.outerHTML = cumulative;
}); });
template._entryMap = new Map(); templateEntryMap = new Map();
template.apply = function(pattern, promise) { template.apply = function(pattern, promise) {
if (typeof pattern !== 'string') { if (typeof pattern !== 'string') {
console.error('pattern must be a string, received: ', pattern); console.error('pattern must be a string, received: ', pattern);
@ -101,7 +101,7 @@ template.apply.applicators = class {
} }
_apply(asyncArgs) { _apply(asyncArgs) {
template._entryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs); templateEntryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs);
} }
values(...args) { values(...args) {
@ -136,7 +136,6 @@ template.apply.applicators = class {
} }
}; };
(() => {
const parseType = (type) => { const parseType = (type) => {
let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/); let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/);
let id = result[1]; let id = result[1];
@ -162,12 +161,12 @@ template.apply.applicators = class {
let promises = []; let promises = [];
let parents = new Set(); let parents = new Set();
for (const child of children) { for (const child of children) {
for (const [pattern, argsCallback] of template._entryMap) { for (const [pattern, argsCallback] of templateEntryMap) {
await argsCallback().then(args => { await argsCallback().then(args => {
if (pattern.test(child.id)) { 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') { 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 { } else {
let params = Array.from(args) let params = Array.from(args)
for (const subtype of child.typeCapture.params) { for (const subtype of child.typeCapture.params) {

View file

@ -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;
}

View file

@ -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 { html, body {
background-color: var(--background-dark); background-color: var(--background-dark);
@ -32,394 +10,27 @@ html, body {
font-family: sans-serif; 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 { #games-page {
max-width: 1920px; max-width: 1920px;
} }
.game-list-page-entry-icon { .list-page-entry-text.game-name {
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;
flex-grow: 1; flex-grow: 1;
flex-basis: 0px;
} }
.game-list-page-entry-description { .list-page-entry-text.game-description {
box-sizing: border-box;
margin: 0;
padding: 0 12px;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 2; flex-grow: 2;
flex-basis: 0px;
} }
#achievements-page { #achievements-page {
max-width: 1920px; max-width: 1920px;
} }
.achievement-list-page-entry-icon { .list-page-entry-text.achievement-name {
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;
flex-grow: 4; flex-grow: 4;
flex-basis: 0px;
} }
.achievement-list-page-entry-description { .list-page-entry-text.achievement-description {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@ -431,22 +42,10 @@ html, body {
white-space: nowrap; white-space: nowrap;
flex-grow: 8; flex-grow: 8;
flex-basis: 0px;
} }
.achievement-list-page-entry-stages { .list-page-entry-text.achievement-stages {
box-sizing: border-box;
margin: 0;
padding: 0 12px;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1; flex-grow: 1;
flex-basis: 0px;
} }
#profile-page { #profile-page {
@ -482,6 +81,8 @@ html, body {
#profile-info-pfp { #profile-info-pfp {
width: 100%; width: 100%;
max-width: 640px; max-width: 640px;
margin-bottom: 1.25em;
} }
#profile-info-name { #profile-info-name {
@ -491,7 +92,78 @@ html, body {
font-size: 42px; 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 { #profile-platforms {
@ -504,74 +176,6 @@ html, body {
flex-grow: 1; 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 { #profile-section-2 {
box-sizing: border-box; box-sizing: border-box;
@ -586,122 +190,16 @@ html, body {
#profile-games { #profile-games {
box-sizing: border-box; box-sizing: border-box;
padding: 0px 64px; padding: 0px 64px 64px;
width: 50%; width: 50%;
height: max-content; 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 { #profile-achievements {
box-sizing: border-box; box-sizing: border-box;
padding: 0px 64px; padding: 0px 64px 64px;
width: 50%; width: 50%;
height: max-content; 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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,38 +1,38 @@
<div class="list-page-search"> <div class="list-page-search">
<label for="achievement-search">Search</label> <label for="achievement-search">Search</label>
<input id="achievement-search" type="text" placeholder="Name, Keyword, etc..." name="achievement-search" /> <input id="achievement-search" type="text" placeholder="Name, Keyword, etc..." achievement-name="achievement-search" />
</div> </div>
<div class="list-page-partitions"> <div class="list-page-partitions">
<div class="list-page-filter-partition"> <div class="list-page-filter-partition">
<p class="page-subheader-text">Filters</p> <p class="page-subheader-text">Filters</p>
<div class="page-subheader-separator"></div> <div class="page-subheader-separator"></div>
<div id="games-owned-filter" class="list-page-filter"> <div id="from-games-owned-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div> <div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">From Games Owned</p> <p class="list-page-filter-name">From Games Owned</p>
</div> </div>
<div id="games-owned-filter" class="list-page-filter"> <div id="in-progress-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div> <div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">In Progress</p> <p class="list-page-filter-name">In Progress</p>
</div> </div>
<div id="games-owned-filter" class="list-page-filter"> <div id="completed-filter" class="list-page-filter">
<div class="list-page-filter-checkbox"></div> <div class="list-page-filter-checkbox"></div>
<p class="list-page-filter-name">Completed</p> <p class="list-page-filter-name">Completed</p>
</div> </div>
</div> </div>
<div class="list-page-list-partition"> <div class="list-page-list-partition">
<div class="list-page-list"> <div class="list-page-list">
<div class="list-page-list-header"> <div class="list-page-header">
<p class="achievement-list-page-entry-icon"></p> <p class="list-page-entry-icon"></p>
<p class="achievement-list-page-entry-name">Name</p> <p class="list-page-entry-text achievement-name">Name</p>
<p class="achievement-list-page-entry-description">Description</p> <p class="list-page-entry-text achievement-description">Description</p>
<p class="achievement-list-page-entry-stages">Stages</p> <p class="list-page-entry-text achievement-stages">Stages</p>
</div> </div>
<template data-template="achievements-page-list: List<Basic>"> <template data-template="achievements-page-list: List<Basic>">
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="achievement-list-page-entry-icon" src="res/dummy_achievement.png" alt="Achievement Thumbnail"></img> <img class="list-page-entry-icon" src="res/dummy_achievement.png" alt="Achievement Thumbnail"></img>
<p class="achievement-list-page-entry-name">${name}</p> <p class="list-page-entry-text achievement-name">${achievement-name}</p>
<p class="achievement-list-page-entry-description">${description}</p> <p class="list-page-entry-text achievement-description">${achievement-description}</p>
<p class="achievement-list-page-entry-stages">${stages}</p> <p class="list-page-entry-text achievement-stages">${stages}</p>
</div> </div>
</template> </template>
</div> </div>

View file

@ -1,6 +1,6 @@
<div class="list-page-search"> <div class="list-page-search">
<label for="game-search">Search</label> <label for="game-search">Search</label>
<input id="game-search" type="text" placeholder="Name, Keyword, etc..." name="game-search" /> <input id="game-search" type="text" placeholder="Name, Keyword, etc..." game-name="game-search" />
</div> </div>
<div class="list-page-partitions"> <div class="list-page-partitions">
<div class="list-page-filter-partition"> <div class="list-page-filter-partition">
@ -13,40 +13,40 @@
</div> </div>
<div class="list-page-list-partition"> <div class="list-page-list-partition">
<div class="list-page-list"> <div class="list-page-list">
<div class="list-page-list-header"> <div class="list-page-header">
<p class="game-list-page-entry-icon"></p> <p class="list-page-entry-icon"></p>
<p class="game-list-page-entry-name">Name</p> <p class="list-page-entry-text game-name">Name</p>
<p class="game-list-page-entry-description">Description</p> <p class="list-page-entry-text game-description">Description</p>
</div> </div>
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="game-list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" /> <img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
<p class="game-list-page-entry-name">Latin</p> <p class="list-page-entry-text game-name">Latin</p>
<p class="game-list-page-entry-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div> </div>
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="game-list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" /> <img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
<p class="game-list-page-entry-name">Latin</p> <p class="list-page-entry-text game-name">Latin</p>
<p class="game-list-page-entry-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div> </div>
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="game-list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" /> <img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
<p class="game-list-page-entry-name">Latin</p> <p class="list-page-entry-text game-name">Latin</p>
<p class="game-list-page-entry-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div> </div>
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="game-list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" /> <img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
<p class="game-list-page-entry-name">Latin</p> <p class="list-page-entry-text game-name">Latin</p>
<p class="game-list-page-entry-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div> </div>
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="game-list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" /> <img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
<p class="game-list-page-entry-name">Latin</p> <p class="list-page-entry-text game-name">Latin</p>
<p class="game-list-page-entry-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div> </div>
<div class="list-page-list-entry"> <div class="list-page-entry">
<img class="game-list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" /> <img class="list-page-entry-icon" src="res/dummy_game.png" alt="Achievement Icon.png" />
<p class="game-list-page-entry-name">Latin</p> <p class="list-page-entry-text game-name">Latin</p>
<p class="game-list-page-entry-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p class="list-page-entry-text game-description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,134 +1,149 @@
<div id="profile-section-1"> <div id="profile-section-1">
<div id="profile-info"> <div id="profile-info">
<img id="profile-info-pfp" src="res/temp_pfp.png" alt="User's Profile Pictuer" /> <img id="profile-info-pfp" src="res/temp_pfp.png" alt="User's Profile Pictuer" />
<div class="page-subheader-separator"></div>
<p id="profile-info-name">Jane Doe</p> <p id="profile-info-name">Jane Doe</p>
</div> </div>
<div id="profile-platforms"> <div id="profile-platforms">
<p class="page-subheader-text">Platforms</p> <p class="page-subheader-text">Platforms</p>
<div class="page-subheader-separator"></div> <div class="page-subheader-separator"></div>
<div class="profile-platform-entry connected"> <div class="profile-list">
<div class="profile-platform-entry-left"> <div class="profile-entry accented">
<img class="profile-platform-icon" src="res/steam.png" alt="Steam Logo" /> <div class="profile-entry-left">
<p class="profile-platform-name">Steam</p> <img class="profile-entry-icon" src="res/steam.png" alt="Steam Logo" />
<p class="profile-entry-text">Steam</p>
</div> </div>
<div class="profile-platform-entry-right"> <div class="profile-entry-right">
<p class="profile-platform-connected">Connected</p> <p class="profile-entry-text accented">Connected</p>
</div> </div>
</div> </div>
<div class="profile-platform-entry"> <div class="profile-entry">
<div class="profile-platform-entry-left"> <div class="profile-entry-left">
<img class="profile-platform-icon" src="res/xbox.png" alt="Xbox Logo" /> <img class="profile-entry-icon" src="res/xbox.png" alt="Xbox Logo" />
<p class="profile-platform-name">Xbox Live</p> <p class="profile-entry-text">Xbox Live</p>
</div> </div>
<div class="profile-platform-entry-right"> <div class="profile-entry-right">
<p class="profile-platform-connected">Connected</p> <p class="profile-entry-text accented">Connected</p>
</div> </div>
</div> </div>
<div class="profile-platform-entry"> <div class="profile-entry">
<div class="profile-platform-entry-left"> <div class="profile-entry-left">
<img class="profile-platform-icon" src="res/psn.png" alt="PSN Logo" /> <img class="profile-entry-icon" src="res/psn.png" alt="PSN Logo" />
<p class="profile-platform-name">PSN</p> <p class="profile-entry-text">PSN</p>
</div>
<div class="profile-entry-right">
<p class="profile-entry-text accented">Connected</p>
</div> </div>
<div class="profile-platform-entry-right">
<p class="profile-platform-connected">Connected</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="profile-section-2"> <div id="profile-section-2">
<div id="profile-games"> <div id="profile-games">
<p class="page-subheader-text">Games</p> <p class="page-subheader-text link">Games</p>
<div class="page-subheader-separator"></div> <div class="page-subheader-separator"></div>
<div class="profile-game-entry"> <div class="profile-list">
<img class="profile-game-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" /> <div class="profile-entry game">
<p class="profile-game-entry-name">Latin</p> <div class="profile-entry-left">
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
<p class="profile-entry-text">Latin</p>
</div> </div>
<div class="profile-game-entry">
<img class="profile-game-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
<p class="profile-game-entry-name">Latin</p>
</div> </div>
<div class="profile-game-entry"> <div class="profile-entry game">
<img class="profile-game-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" /> <div class="profile-entry-left">
<p class="profile-game-entry-name">Latin</p> <img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
<p class="profile-entry-text">Latin</p>
</div>
</div>
<div class="profile-entry game">
<div class="profile-entry-left">
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
<p class="profile-entry-text">Latin</p>
</div>
</div>
<div class="profile-entry game">
<div class="profile-entry-left">
<img class="profile-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
<p class="profile-entry-text">Latin</p>
</div>
</div> </div>
<div class="profile-game-entry">
<img class="profile-game-entry-icon" src="res/dummy_game.png" alt="Some Game Icon" />
<p class="profile-game-entry-name">Latin</p>
</div> </div>
</div> </div>
<div id="profile-achievements"> <div id="profile-achievements">
<p class="page-subheader-text">Achievements</p> <p class="page-subheader-text link">Achievements</p>
<div class="page-subheader-separator"></div> <div class="page-subheader-separator"></div>
<div class="profile-achievement-entry completed"> <div class="profile-list">
<div class="profile-achievement-entry-left"> <div class="profile-entry accented">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <div class="profile-entry-left">
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry completed"> <div class="profile-entry accented">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry completed"> <div class="profile-entry accented">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry"> <div class="profile-entry">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry"> <div class="profile-entry">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry"> <div class="profile-entry">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry"> <div class="profile-entry">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div> </div>
<div class="profile-achievement-entry-right"> <div class="profile-entry-right">
<p class="profile-achievement-completed">Completed</p> <p class="profile-entry-text accented">Completed</p>
</div> </div>
</div> </div>
<div class="profile-achievement-entry"> <div class="profile-entry">
<div class="profile-achievement-entry-left"> <div class="profile-entry-left">
<img class="profile-achievement-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" /> <img class="profile-entry-icon" src="res/dummy_achievement.png" alt="Some Achievement Icon" />
<p class="profile-achievement-entry-name">Lorem Ipsum</p> <p class="profile-entry-text">Lorem Ipsum</p>
</div>
<div class="profile-entry-right">
<p class="profile-entry-text accented">Completed</p>
</div> </div>
<div class="profile-achievement-entry-right">
<p class="profile-achievement-completed">Completed</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -31,7 +31,8 @@ CREATE TABLE [User] (
ID INT IDENTITY(0, 1) NOT NULL, ID INT IDENTITY(0, 1) NOT NULL,
Email VARCHAR(254) NOT NULL, Email VARCHAR(254) NOT NULL,
Username VARCHAR(32) NOT NULL, Username VARCHAR(32) NOT NULL,
[Password] CHAR(256) NOT NULL [Password] CHAR(64) NOT NULL,
[Salt] CHAR(32) NOT NULL
PRIMARY KEY(ID) PRIMARY KEY(ID)
) )

Binary file not shown.