Merge pull request #1 from ArloFilley/docker

Docker Support
This commit is contained in:
Arlo Filley 2024-03-20 17:07:31 +00:00 committed by GitHub
commit 51cd9ac2b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 2906 additions and 123 deletions

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM rust:1 AS builder
WORKDIR /app
COPY . .
RUN apt-get -y update && apt-get -y upgrade
RUN apt-get install -y sqlite3 libsqlite3-dev
ENV DATABASE_URL="sqlite:/app/database/dev/database.sqlite"
RUN cargo build --release
FROM ubuntu:latest as runner
WORKDIR /app
COPY ./wordlist.txt ./wordlist.txt
COPY ./Rocket.toml ./Rocket.toml
COPY --from=builder /app/target/release/cs_coursework /usr/local/bin/webapp
ENV DATABASE_URL="sqlite:/app/database/dev/database.sqlite"
CMD [ "webapp" ]

23
Rocket.toml Normal file
View File

@ -0,0 +1,23 @@
[default]
address = "::"
port = 8000
workers = 4
max_blocking = 512
keep_alive = 5
ident = "Rocket"
log_level = "normal"
temp_dir = "/tmp"
cli_colors = true
secret_key = "Change This"
[default.limits]
form = "64 kB"
json = "1 MiB"
msgpack = "2 MiB"
"file/jpg" = "5 MiB"
[default.shutdown]
ctrlc = true
signals = ["term", "hup"]
grace = 5
mercy = 5

View File

@ -7,6 +7,9 @@
-[ ] Make js api asynchronous
-[ ] Hash passwords on front end so server plaintext password is never sent over the internet
-[ ] Use secret to uniquely identify
-[ ] Scrollbars
-[x] Scrollbars
-[ ] Change Color Scheme
-[ ] Be able to view others accounts
-[ ] Be able to view others accounts
-[ ] Username gets cut off if too long
-[ ] Validate username length
-[ ] Test caret doesn't keep up

Binary file not shown.

41
about/TODO.md Normal file
View File

@ -0,0 +1,41 @@
# Backend
-[x] Migrate to sqlx as backend database queryer
-[ ] Allow Private Accounts
-[ ] Secrets to authenicate that its the user
-[ ] Account Profile
-[ ] Better Password Security
-[ ] Login with google
-[ ] Give new test route a more descriptive name
-[x] Remove need for cors
# Frontend
## API
-[ ] Make js api asynchronous
-[ ] Hash passwords on front end so server plaintext password is never sent over the internet
-[x] Use secret to uniquely identify users
## Components
-[x] Scrollbars
-[ ] Scrollbars shouldn't appear if there's not enought content
-[ ] Scrollbar update
-[ ] Account Component
-[ ] Username gets cut off if too long
-[ ] Test caret doesn't display properley
## Screens
-[x] Change Color Scheme
-[ ] Color Scheme Editor
-[ ] Be able to view others accounts
## Users
-[ ] Validate username length
-[ ] Validate usernames
-[x] Login bug, text goes wrong color when removeing all text from a box
## API
-[ ] Give useful errors to the user when something goes wrong
## Admin
-[ ] Authentication
-[ ] Allow deleting users
-[ ] Allow deleting tests

Binary file not shown.

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<link rel="stylesheet" href="../index.css">
<script src="index.js"></script>
</head>
<body>
<nav>
<p>Arlo Filley</p>
<ul>
<li><a href="../">Home</a></li>
<li><a href="./">Delete A User</a></li>
</ul>
</nav>
<h1>Password</h1>
<input id="password" type="password">
<table>
<tbody id="table"></tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,51 @@
get();
function get() {
let xhr = new XMLHttpRequest();
xhr.open('GET', `https://arlofilley.com/api/typing/leaderboard`);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.send();
xhr.onload = () => {
json = xhr.response
createTable(json);
};
}
function createTable(pJson) {
let table = document.getElementById("table");
console.log(password)
pJson.forEach(element => {
let tr = document.createElement('tr');
let button = document.createElement('button');
let username = element.username;
button.textContent = "delete"
button.addEventListener( "click", () => {
let xhr = new XMLHttpRequest();
let password = document.getElementById("password").value;
xhr.open('GET', `https://arlofilley.com/api/typing/delete_user/${password}/${username}/120932187`);
xhr.send();
})
create_table_element(tr, "", [element.username, element.wpm], button)
table.appendChild(tr)
});
}
function create_table_element(tr, string, elements, button) {
if (elements.length > 0) {
for (let i = 0; i < elements.length; i++) {
let td = document.createElement('td');
td.appendChild(document.createTextNode(elements[i]));
tr.appendChild(td);
}
} else {
let td = document.createElement('td');
td.appendChild(document.createTextNode(string));
tr.appendChild(td);
}
tr.appendChild(button);
}

125
public/Admin/index.css Executable file
View File

@ -0,0 +1,125 @@
:root {
--navbar: #333333;
/* Navbar Button Properties */
--navbar-button: #058ED9;
--navbar-button-hover: #283F3B;
--navbar-button-border: #058ED9;
/* */
--background: #1f1f1f;
/* Table Properties*/
--table: #FFD07B;
--table-top:#FDB833;
/* Title Properties */
--title-background: #333333;
--title-text: #ffffff;
/* General Properties */
--text: #000000;
--border: #000000;
}
body {
background-color: #111;
margin: 0;
padding: 0;
}
nav {
background-color: #333;
height: 75px;
display: flex;
justify-content: center;
align-items: center;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
nav p {
margin-right: 700px;
text-decoration: none;
color: #fff;
font-size: 38px;
font-weight: bold;
text-transform: uppercase;
font-family: "Montserrat", sans-serif;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
display: inline-block;
margin: 0 20px;
}
a {
text-decoration: none;
color: #fff;
font-size: 18px;
font-weight: bold;
text-transform: uppercase;
font-family: "Montserrat", sans-serif;
padding: 10px 20px;
transition: background-color 0.3s ease;
}
a:hover {
background-color: #555;
}
h1 {
margin-left: 5%;
margin-bottom: 0%;
color: #fff;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
input {
margin-left: 5%;
margin-top: 1.5%;
}
table {
width: 90%;
border-collapse: collapse;
margin: 0px;
margin-left: 5%;
margin-top: 30px;
padding: 0px;
border-spacing: 0px;
border: 2px var(--border) solid;
border-radius: 20px;
}
table tr:nth-child(n) {
background: var(--table);
}
table tr:nth-child(1) {
background: var(--table-top);
}
tr {
border: 1px black dotted;
}
td {
text-align: center;
padding: 10px;
font-size: larger;
}
button {
margin-left: 40%;
padding: 8px;
padding-left: 12.5%;
padding-right: 12.5%;
}

20
public/Admin/index.html Executable file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<nav>
<p>Arlo Filley</p>
<ul>
<li><a href="./">Home</a></li>
<li><a href="./Delete User/">Delete A User</a></li>
</ul>
</nav>
</body>
</html>

292
public/api/api.js Normal file
View File

@ -0,0 +1,292 @@
/**
* @file This file provides abstracted functions to interact with the api
* @author Arlo Filley
*
*/
/**
* This class provides all the useful methods to interact with the api.
*/
class API {
constructor() { this.url = "/api"; }
/**
* This takes the validated data and makes a post
* request to the rocket server
* @param {String} testType
* @param {int} testLength
* @param {int} testTime
* @param {int} testSeed
* @param {int} quoteId
* @param {int} wpm
* @param {int} accuracy
* @param {int} userId
*/
postTest(pTestType, pTestLength, pTestTime, pTestSeed, pQuoteId, pWpm, pAccuracy, pUserId) {
const data = {
'test_type': pTestType,
'test_length': pTestLength,
'test_time': pTestTime,
'test_seed': pTestSeed,
'quote_id': pQuoteId,
'wpm': pWpm,
'accuracy': pAccuracy,
'user_id': pUserId,
'secret': user.secret
}
const xhr = new XMLHttpRequest();
xhr.open(
"POST",
`${this.url}/post_test/`
);
xhr.send(
JSON.stringify(data)
);
user.lastTest = data;
}
/**
* Validates all the parameters used for the postTest function which it then calls
*/
validateTest() {
const test = screenManager.screen.textbox.getWords();
const testType = "words";
let testLength = test.length;
let testTime = screenManager.screen.timer.getTime();
const testSeed = 0;
const quoteId = 0;
let wpm;
const userId = Number(user.userId);
let test_content = screenManager.screen.textbox.getTestContent();
let string = "";
let inaccurateLetters = 0;
for (let letter = 0; letter < test.length; letter++) {
if (test[letter] === test_content[letter]) {
string += test[letter];
} else {
inaccurateLetters += 1;
}
}
const accuracy = Math.round(((test.length - inaccurateLetters) / test.length) * 100);
// this is the wpm calculation factoring in the time of test
// it assumes that all words are 5 characters long because on average
// they are
wpm = Math.round((string.length / 5) * (60 / testTime));
// the following code is a series of if statements that checks the
// types of the variables is correct if not it errors it and returns
// out of the function
if ( typeof testType !== "string" ) {
console.error(`testType is value ${typeof testType}\nshould be a string`);
return;
}
if ( typeof testLength !== "number") {
console.error(`testLength is value ${typeof testLength}\n should be a number`);
return;
}
if ( typeof testTime !== "number") {
console.error(`testTime is value ${typeof testTime}\n should be a number`);
return;
}
if ( typeof testSeed !== "number") {
console.error(`testSeed is value ${typeof testSeed}\n should be a number`);
return;
}
if ( typeof quoteId !== "number") {
console.error(`quoteId is value ${typeof quoteId}\n should be a number`);
return;
}
if ( typeof wpm !== "number") {
console.error(`wpm is value ${typeof wpm}\n should be a number`);
return;
}
if ( typeof accuracy !== "number") {
console.error(`accuracy is value ${typeof accuracy}\n should be a number`);
return;
}
if ( typeof userId !== "number") {
console.error(`userId is value ${typeof userId}\n should be a number`);
return;
}
// after checking that all variables are of the correct type these if statements check
// that they are acceptable values or are in acceptable bounds depending on variable types
if (testType !== "words") {
// currently words is the only acceptable type but
// this will change in later iterations
console.error(`testType is invalid\nacceptable options ['words']`);
}
// upper bounds for these numbers are less of a concern because the server will automatically
// return an error if values are over the limit
if (testLength < 0) {
console.error(`testLength is too small, min value 0`)
}
if (testTime < 1) {
console.error(`testTime is too small, min value 1`)
}
if (testSeed < 0) {
console.error(`testSeed is too small, min value 0`)
}
if (quoteId < 0) {
console.error(`quoteId is too small, min value 0`)
}
if (wpm < 0) {
console.error(`wpm is too small, min value 0`)
}
// accuracy needs an upper bound check because users can't have more than 100%
// accuracy when completing their tests
if (accuracy < 0) {
console.error(`accuracy is too small, min value 0`)
} else if (accuracy > 100) {
console.error(`accuracy is too big, max value 100`)
}
if (userId < 0) {
console.error(`userId is too small, min value 0`)
}
// there will be other tests here in later iterations but for now these tests should suffice
this.postTest(testType, testLength, testTime, testSeed, quoteId, wpm, accuracy, userId);
}
/**
* takes a validated name and password and sends
* a post request to make a user with the given
* username and password
* @param {String} username
* @param {String} password
* @returns
*/
createUser( username, password ) {
console.log( username, password );
const user = {
username: username,
password: password
};
const xhr = new XMLHttpRequest();
xhr.open( "POST", `${this.url}/create_user/` );
xhr.send( JSON.stringify(user) );
xhr.onload = () => {
if (xhr.status === 500) {
alert("Sorry, looks like your username isn't unique");
console.error("Sorry, looks like your username isn't unique")
} else {
this.login(username, password);
}
};
}
/**
* takes a validated name and password and sends
* a post request to make a user with the given
* username and password
* @param {String} username
* @param {String} password
* @param {boolean} initial
* @returns
*/
login(pUsername, pPassword, initial = false) {
// If Local Storage has the information we need there is no need to make a request to the server
if (localStorage.getItem("username") === pUsername || (initial && localStorage.length === 3) ) {
user.userId = localStorage.getItem("userId");
user.secret = localStorage.getItem("secret");
user.username = localStorage.getItem("username");
return
}
// Variable Validation
if (pUsername == undefined || pPassword == undefined) {
return
}
let xhr = new XMLHttpRequest();
xhr.open('GET', `${this.url}/login/${pUsername}/${pPassword}`);
xhr.send();
xhr.onload = () => {
let response = JSON.parse(xhr.response);
// If there is an error with the login we need
if (xhr.response === null) {
alert("Error Logging in, maybe check your password");
return
}
user.userId = response.user_id;
user.username = pUsername
user.secret = response.secret;
localStorage.setItem("userId", user.userId);
localStorage.setItem("username", pUsername);
localStorage.setItem("secret", user.secret);
};
}
logout() {
user = new User();
user.username = "no one";
user.password = "";
user.userId = 0;
user.tests = [];
localStorage.clear();
this.getTest();
}
getUserTests() {
if (user.userId === 0) {
user.tests = undefined;
return;
}
let xhr = new XMLHttpRequest();
xhr.open('GET', `${this.url}/get_tests/${user.userId}/${user.secret}`);
xhr.send();
xhr.onload = () => {
user.tests = JSON.parse(xhr.response);
};
}
getLeaderBoard() {
let xhr = new XMLHttpRequest();
xhr.open('GET', `${this.url}/leaderboard/`);
xhr.send();
xhr.onload = () => {
user.leaderboard = JSON.parse(xhr.response);
};
}
getTest() {
let xhr = new XMLHttpRequest();
xhr.open('GET', `${this.url}/new_test/`);
xhr.send();
xhr.onload = () =>{
const effectiveWidth = (windowWidth - 200) / 13;
let textArr = JSON.parse(xhr.response);
let finalText = [];
let text = "";
for (let i = 0; i < textArr.length; i++) {
if (text.length + textArr[i].length < effectiveWidth) {
text += `${textArr[i]} `
} else {
finalText.push(text.substring(0,text.length-1));
text = `${textArr[i]} `;
}
}
user.nextTest = finalText;
};
}
}

42
public/api/user.js Executable file
View File

@ -0,0 +1,42 @@
/**
* @file This file provides an abstraction of all the data about the user
* @author Arlo Filley
*/
/**
* this class displays a number of textboxes that allows the user to input a
* username and password. Then find out the user_id of that account through the
* necessary api routes.
*/
class User {
constructor() {
this.username = "not logged in";
this.userId = 0;
this.secret;
this.leaderboard;
this.time = 15;
this.tests;
this.lastTest;
this.nextTest = null;
this.colorScheme = {
background: "#121212",
text: "#AAA",
timerBar: "#50C5B7",
timerText: "#000",
testGood: "#0A0",
testBad: "#A00",
buttonBG: "#12202f",
buttonText: "#fff",
buttonBorder: "#000",
buttonHoverBG: "#FFF",
buttonHoverText: "#000",
buttonHoverBorder: "#000"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

BIN
public/assets/favicon/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"/></svg>

After

Width:  |  Height:  |  Size: 732 B

215
public/components/button.js Executable file
View File

@ -0,0 +1,215 @@
/**
* @file This file provides the button class, which can
* be checked for clicks
* @author Arlo Filley
*
* TODO:
* - implement visual changes (borders, etc)
* - replace with methods with getters and setters
*/
/**
* Button class, a rectangle that can be checked for mouse clicks
*/
class Button {
// this is the doc comment for the Timer class
/**
* @param {int} pX
* @param {int} pY
* @param {int} pWidth
* @param {int} pHeight
* @param {int} pLayer
* @param {bool} pVisible
* @param {hexcode} pTextColor
* @param {bool} pBorder
* @param {hexcode} pBorderColor
* @param {hexcode} pBackgroundColor
* @param {int} pTime
* @param {bool} pBar
* @param {string} Label
*/
constructor(pX = 100, pY = 100,
pWidth = 200, pHeight = 30,
pLabel = "Default Button",
pBorder = false,
pHoverBorder = true,
pTextColor = user.colorScheme.buttonText,
pBorderColor = user.colorScheme.buttonBorder,
pBackgroundColor = user.colorScheme.buttonBG,
pHoverTextColor = user.colorScheme.buttonHoverText,
pHoverBorderColor = user.colorScheme.buttonHoverBorder,
pHoverBackgroundColor = user.colorScheme.buttonHoverBG,
pLayer = 0, pVisible = true,
) {
this.x = pX;
this.y = pY;
this.width = pWidth;
this.height = pHeight;
this.layer = pLayer;
this.visible = pVisible;
this.textColor = pTextColor;
this.border = pBorder;
this.borderColor = pBorderColor;
this.backgroundColor = pBackgroundColor;
this.label = pLabel;
// Attributes to control the look of the button
// when the user is hovering over it
this.hoverBorder = pHoverBorder;
this.hoverBorderColor = pHoverBorderColor;
this.hoverTextColor = pHoverTextColor;
this.hoverBackgroundColor = pHoverBackgroundColor;
}
getx() {
return this.x;
}
setX(pX) {
this.x = pX;
}
getY() {
return this.y;
}
setY(pY) {
this.y = pY;
}
getWidth() {
return this.width;
}
setWidth(pWidth) {
this.width = pWidth;
}
getHeight() {
return this.height;
}
setHeight(pHeight) {
this.height = pHeight;
}
getLayer() {
return this.layer;
}
setLayer(pLayer) {
this.layer = pLayer;
}
getVisible() {
return this.visible;
}
setVisible(pVisible) {
this.visible = pVisible;
}
getTextColor() {
return this.textColor;
}
setTextColor(pTextColor) {
this.textColor = pTextColor;
}
getBorder() {
return this.border;
}
setBorder(pBorder) {
this.border = pBorder;
}
getBorderColor() {
return this.borderColor;
}
setBorderColor(pBorderColor) {
this.borderColor = pBorderColor;
}
getBackgroundColor() {
return this.backgroundColor;
}
setBackgroundColor(pBackgroundColor) {
this.backgroundColor = pBackgroundColor;
}
getLabel() {
return this.label;
}
setLabel(pLabel) {
this.label = pLabel;
}
/**
* This functions returns more
*/
isPressed() {
if (!this.visible) {
return;
} else if (!mouseIsPressed) { // a unique p5.js value that checks if the mouse is clicked
return false;
}
// if the mouse is within the bounds of the return that the button has been pressed
if (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height) {
return true;
}
return false;
}
/**
* This function draws the button with the label
*/
draw() {
if (!this.visible) {
return;
}
textSize(20);
textAlign(CENTER, CENTER);
if (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height) {
if (this.hoverBorder) {
strokeWeight(2);
stroke(this.hoverBorderColor)
} else {
noStroke();
}
fill(this.hoverBackgroundColor);
rect(this.x, this.y, this.width, this.height);
noStroke();
fill(this.hoverTextColor);
text(this.label, this.x, this.y, this.width, this.height);
} else {
if (this.border) {
strokeWeight(2);
stroke(this.borderColor)
} else {
noStroke();
}
fill(this.backgroundColor);
rect(this.x, this.y, this.width, this.height);
noStroke();
fill(this.textColor);
text(this.label, this.x, this.y, this.width, this.height);
}
noStroke();
}
}

31
public/components/canvas.js Executable file
View File

@ -0,0 +1,31 @@
/**
* @file This file provides a canvas class wrapper for the p5.js canvas
* @author Arlo Filley
*
*/
/**
* this class provides a wrapper around the
* p5.js canvas, with easier methods to work with.
*/
class Canvas {
constructor() {
this.x = 0;
this.y = 0;
this.canvas = createCanvas(0, 0);
}
center() {
this.canvas.position(this.x, this.y);
}
resize() {
this.canvas.resize(windowWidth, windowHeight);
}
disable() {
this.canvas.resize(0, 0);
}
}

44
public/components/menu.js Executable file
View File

@ -0,0 +1,44 @@
/**
* @file This file provides a menu class to allow the user to easily navigate the site
* @author Arlo Filley
*
* TODO:
* - more sensible button names for easier navigation
*/
/**
* this class provides a menu with all the relevent buttons the user will need,
* it also handles when the user presses a button, by creating the correct screen
*/
class Menu {
constructor() {
this.numButtons = 5;
this.user = new UserShower((windowWidth / this.numButtons) * 0, 0, windowWidth / this.numButtons, 50);
this.buttons = [
// new Button((windowWidth / this.numButtons) * 0, 0, windowWidth / 5, 50, "Account"),
new Button((windowWidth / this.numButtons) * 1, 0, windowWidth / this.numButtons, 50, "Test Data"),
new Button((windowWidth / this.numButtons) * 2, 0, windowWidth / this.numButtons, 50, "Start Test"),
new Button((windowWidth / this.numButtons) * 3, 0, windowWidth / this.numButtons, 50, "Leaderboard"),
new Button((windowWidth / this.numButtons) * 4, 0, windowWidth / this.numButtons, 50, "Test Settings")
]
}
draw() {
textAlign(CENTER, CENTER);
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw()
}
if (this.buttons[0].isPressed()) {
screenManager.setScreen(new ProfileScreen());
} else if (this.buttons[1].isPressed()) {
screenManager.setScreen(new TestScreen())
} else if (this.buttons[2].isPressed()) {
screenManager.setScreen(new LeaderboardScreen())
} else if (this.buttons[3].isPressed()) {
screenManager.setScreen(new SettingsScreen())
}
this.user.draw()
}
}

364
public/components/textbox.js Executable file
View File

@ -0,0 +1,364 @@
/**
* @file This file provides the textbox class for taking user input
* @author Arlo Filley
*
* TODO:
* - add all characters a user could press
* - refactor the code displaying the characters. It can become slow after lots of typing
* - password mode, where the charcters are hidden from the user
* - getters and setters
*/
/**
* This class takes input from the user and displays it some form
* it handles the test input from the user, and the login and sign
* up pages.
*/
class Textbox {
/**
* Creates a new instance of the Textbox class
* @param {int} pX
* @param {int} pY
* @param {int} pWidth
* @param {int} pHeight
* @param {int} pLayer
* @param {bool} pVisible
* @param {hexcode} pTextColor
* @param {bool} pBorder
* @param {hexcode} pBorderColor
* @param {hexcode} pBackgroundColor
* @param {bool} pLine
* @param {bool} pIsTest
* @param {bool} pIsPassword
*/
constructor(
pX, pY,
pWidth, pHeight,
pLayer, pVisible,
pTextColor = user.colorScheme.text,
pBorder, pBorderColor,
pBackgroundColor,
pLine, pIsTest,
pIsPassword = false
) {
this.x = pX;
this.y = pY;
this.width = pWidth;
this.height = pHeight;
this.layer = pLayer;
this.visible = pVisible;
this.textColor = pTextColor;
this.border = pBorder;
this.borderColor = pBorderColor;
this.backgroundColor = pBackgroundColor;
this.letters = [];
this.allowedLetters = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm','n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
'x', 'y', 'z',
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'\'', '"', ',', '.', ' ', '!', '@', '$', '%', '^', '&', '*', '(', ')',
]
this.line = pLine;
this.isTest = pIsTest;
if (this.isTest) {
this.testContent = user.nextTest;
this.currentLine = 0;
this.words = [""];
} else {
this.words = "";
}
this.isPassword = pIsPassword;
this.goodColor = user.colorScheme.testGood;
this.badColor = user.colorScheme.testBad;
this.textColor = user.colorScheme.text;
}
getX() {
return this.x;
}
setX(pX) {
this.x = pX;
}
getY() {
return this.y;
}
setY(pY) {
this.y = pY;
}
getWidth() {
return this.width;
}
setWidth(pWidth) {
this.width = pWidth;
}
getHeight() {
return this.height;
}
setHeight(pHeight) {
this.height = pHeight;
}
getLayer() {
return this.layer;
}
setLayer(pLayer) {
this.layer = pLayer;
}
getVisible() {
return this.visible;
}
setVisible(pVisible) {
this.visible = pVisible;
}
getTextColor() {
return this.textColor;
}
setTextColor(pTextColor) {
this.textColor = pTextColor;
}
getBorder() {
return this.border;
}
setBorder(pBorder) {
this.border = pBorder;
}
getBorderColor() {
return this.borderColor;
}
setBorderColor(pBorderColor) {
this.borderColor = pBorderColor;
}
getBackgroundColor() {
return this.backgroundColor;
}
setBackgroundColor(pBackgroundColor) {
this.backgroundColor = pBackgroundColor;
}
getLetters() {
return this.letters;
}
setLetters(pLetters) {
this.letters = pLetters;
}
/**
* takes a key and handles it in the textbox
* @param {String} pKey
* @returns
*/
letterTyped(pKey) {
if (pKey === "Backspace" && this.letters.length > 0) {
this.letters.pop();
if (this.isTest) {
this.words[this.currentLine] = this.words[this.currentLine].substring(0, this.words[this.currentLine].length-1);
} else {
this.words = this.words.substring(0, this.words.length-1)
}
return;
}
for (let i = 0; i < this.allowedLetters.length; i++) {
if (pKey.toLowerCase() === this.allowedLetters[i]) {
this.letters.push(pKey);
if (this.isTest) {
this.words[this.currentLine] += pKey;
} else {
this.words += pKey;
}
return;
}
}
}
getWords() {
let text = "";
for (let i = 0; i < this.words.length; i++) {
text += this.words[i];
}
return text;
}
setWords(pWords) {
this.words = pWords;
}
getAllowedLetters() {
return this.allowedLetters;
}
setAllowedLetters(pAllowedLetters) {
this.allowedLetters = pAllowedLetters;
}
getTestContent() {
let text = "";
for (let i = 0; i < this.testContent.length; i++) {
text += this.testContent[i];
}
return text;
}
/**
* draws a Textbox
* @returns
*/
draw() {
// doesn't render the textbox if it should not be visible to the user.
if (!this.visible) {
return;
}
noStroke();
// sets a border if there should be one
if (this.border) {
stroke(this.borderColor);
strokeWeight(1);
}
// sets the parameters of what the text should look like;
fill(this.textColor);
textSize(23);
textAlign(LEFT);
if (this.words.length === 0 && this.line) {
fill("#000")
rect(this.x, this.y-15, 1, 30)
}
// these variables allow me to use the values of x and y while updating them
let i = this.x;
let j = this.y;
if (this.isTest) {
let i = this.x;
let j = this.y;
if (this.words[this.currentLine].length >= this.testContent[this.currentLine].length) {
this.currentLine++;
this.words.push("");
}
if (this.currentLine > 0) {
for (let x = 0; x < this.testContent[this.currentLine-1].length; x++) {
if (x < this.words[this.currentLine-1].length) {
if (this.words[this.currentLine-1][x] === this.testContent[this.currentLine-1][x]) {
fill("#00AA0044");
} else {
fill("#AA000044");
}
} else {
fill("#00044");
}
text(this.testContent[this.currentLine-1][x], i, j);
i += 13;
}
j+= 30;
}
i = this.x;
for (let x = 0; x < this.testContent[this.currentLine].length; x++) {
if (x < this.words[this.currentLine].length) {
if (this.words[this.currentLine][x] === this.testContent[this.currentLine][x]) {
fill(this.goodColor);
} else {
fill(this.badColor);
}
} else {
fill(this.textColor);
}
text(this.testContent[this.currentLine][x], i, j);
if (this.letters.length > 0 && x == this.letters.length && this.line) {
fill(this.textColor)
rect(i, j-15, 1, 30)
}
i += 13;
}
i = this.x;
j += 30;
fill(this.textColor);
for (let x = this.currentLine + 1; x < this.testContent.length; x++) {
text(this.testContent[x], i, j);
j += 30;
}
} else if (this.isPassword) {
fill(this.textColor);
// these variables allow me to use the values of x and y while updating them
let i = this.x;
let j = this.y;
for (let x = 0; x < this.letters.length; x++) {
if (i > this.x + this.width) i = this.x, j += 30;
if (this.letters[x] === "Enter") {
i = this.x, j+= 30;
} else {
let char = "-";
text(char, i, j);
i += 13
}
if (this.letters.length > 0 && x == this.letters.length-1 && this.line) {
rect(i, j-15, 1, 30)
}
}
} else {
// these variables allow me to use the values of x and y while updating them
let i = this.x;
let j = this.y;
// currently this loop just prints out every letter in the array, including any enter characters
for (let x = 0; x < this.letters.length; x++) {
if (i > this.x + this.width) i = this.x, j += 30;
if (this.letters[x] === "Enter") {
i = this.x, j+= 30;
} else {
text(this.letters[x], i, j);
i += 13
}
if (this.letters.length > 0 && x == this.letters.length-1 && this.line) {
fill(this.textColor)
rect(i, j-15, 1, 30)
}
}
}
}
draw_line(line, y) {
}
}

61
public/components/timemenu.js Executable file
View File

@ -0,0 +1,61 @@
/**
* @file This file provides a time menu class for editing the length of a test
* @author Arlo Filley
*
* TODO:
* - implement visual changes (borders, etc)
* - replace with methods with getters and setters
* - highlight which option the user has chosen in some way
*/
/**
* this class displays a dropdown menu for the user where
* they can edit the duration of a test
*/
class TimeMenu {
constructor() {
this.buttons = [
new Button(900, 250, 100, 30, "15s"),
new Button(900, 280, 100, 30, "30s"),
new Button(900, 310, 100, 30, "45s"),
new Button(900, 340, 100, 30, "60s"),
];
this.topButton = this.buttons[0];
this.dropDownButton = new Button(1000, 250, 30, 30, "v")
this.dropdown = false;
}
draw() {
if (this.dropdown) {
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw()
}
if (this.buttons[0].isPressed() && user.time != 15) {
user.time = 15;
this.topButton = new Button(900, 250, 100, 30, "15s");
this.dropdown = false;
} else if (this.buttons[1].isPressed()) {
user.time = 30;
this.topButton = new Button(900, 250, 100, 30, "30s");
this.dropdown = false;
} else if (this.buttons[2].isPressed()) {
user.time = 45;
this.topButton = new Button(900, 250, 100, 30, "45s");
this.dropdown = false;
} else if (this.buttons[3].isPressed()) {
user.time = 60;
this.topButton = new Button(900, 250, 100, 30, "60s");
this.dropdown = false;
}
} else {
this.topButton.draw();
}
this.dropDownButton.draw();
if (this.dropDownButton.isPressed()) {
this.dropdown = true;
} else if (mouseIsPressed) { this.dropdown = false };
}
}

222
public/components/timer.js Executable file
View File

@ -0,0 +1,222 @@
/**
* @file This file provides a time menu class for editing the length of a test
* @author Arlo Filley
*
* TODO:
* - implement visual changes (borders, etc)
* - fix the timer number becoming invisible after a
* it drops below a certain amount of time
* - use getters and setters
* - use the millis() p5.js function for if the framerate becomes
* slowed down by the amount being drawn to the screen
*/
/**
* This class provides the timer, which handles when a test starts and ends as well
* as providing a visual element for the user to see
*/
class Timer {
/**
* @param {int} pX
* @param {int} pY
* @param {int} pWidth
* @param {int} pHeight
* @param {int} pLayer
* @param {bool} pVisible
* @param {hexcode} pTextColor
* @param {bool} pBorder
* @param {hexcode} pBorderColor
* @param {hexcode} pBackgroundColor
* @param {int} pTime
* @param {bool} pBar
*/
constructor(pX, pY, pWidth, pHeight, pLayer, pVisible, pTextColor, pBorder, pBorderColor, pBackgroundColor, pTime, pBar) {
this.x = pX;
this.y = pY;
this.width = pWidth;
this.height = pHeight;
this.layer = pLayer;
this.visible = pVisible;
this.textColor = pTextColor;
this.border = pBorder;
this.borderColor = pBorderColor;
this.backgroundColor = pBackgroundColor;
this.bar = pBar;
this.startTime;
this.time = pTime;
this.timeElapsed = 0;
this.ended;
this.hasStarted = false;
}
getX() {
return this.x;
}
setX(pX) {
this.x = pX;
}
getY() {
return this.y;
}
setY(pY) {
this.y = pY;
}
getWidth() {
return this.width;
}
setWidth(pWidth) {
this.width = pWidth;
}
getHeight() {
return this.height;
}
setHeight(pHeight) {
this.height = pHeight;
}
getLayer() {
return this.layer;
}
setLayer(pLayer) {
this.layer = pLayer;
}
getVisible() {
return this.visible;
}
setVisible(pVisible) {
this.visible = pVisible;
}
getTextColor() {
return this.textColor;
}
setTextColor(pTextColor) {
this.textColor = pTextColor;
}
getBorder() {
return this.border;
}
setBorder(pBorder) {
this.border = pBorder;
}
getBorderColor() {
return this.borderColor;
}
setBorderColor(pBorderColor) {
this.borderColor = pBorderColor;
}
getBackgroundColor() {
return this.backgroundColor;
}
setBackgroundColor(pBackgroundColor) {
this.backgroundColor = pBackgroundColor;
}
/**
* gets the amount of time the timer will run for
*/
getTime() {
return this.time;
}
/**
* sets the amount of time the timer will run for
* @param {int} pTime
*/
setTime(pTime) {
this.time = pTime;
}
/**
* This method is called to start the timer
*/
start() {
this.startTime = millis();
// framecount is a special p5 value that counts the number of frames that have passed
// I am using the amount of frames passed to calculate the time, assuming that the website is running at 60q frames
// per second
this.timeElapsed = 0;
this.hasStarted = true;
}
/**
* This method should be called once per frame
* it keeps track of the amount of time passed
*/
tick() {
this.timeElapsed = (millis() - this.startTime) / 1000;
if (this.timeElapsed >= this.time) {
this.end();
};
}
/**
* this function is called at the end of the timer
*/
end() {
this.visible = false;
api.validateTest();
this.timeElapsed = 0;
this.time = 0;
api.getTest();
// Then this function will call all other functions necessary to complete the test
// this will likely including changing the screen and interacting with the api
screenManager.setScreen(new EndScreen());
}
/**
* Draws the timer, uses the attributes of the class as options
*/
draw() {
// if the time shouldn't be rendered it quickly exits out of this method
if (!this.visible) return;
textAlign(LEFT);
// adds a border for the bar if one is needed
if (this.border && this.bar) {
strokeWeight(1);
stroke(this.borderColor);
// this doesn't use the fill function like other drawings
// but this adds the necessary color to the border
} else {
noStroke();
}
// draws a bar that move across the screen to show the time left
if (this.bar) {
fill(user.colorScheme.timerBar);
if (this.hasStarted) {
rect(this.y, this.x, windowWidth - windowWidth * (this.timeElapsed / this.time), this.height);
} else {
rect(this.y, this.x, windowWidth, this.height);
}
}
// draws the text in the corner of the screen
noStroke();
fill(user.colorScheme.timerText);
if (this.hasStarted) {
text(Math.ceil(this.time - this.timeElapsed), this.x + this.width / 6, this.y + this.height / 2);
} else {
text("Type A Letter To Start", this.x + this.width / 6, this.y + this.height / 2);
}
}
}

35
public/components/user.js Normal file
View File

@ -0,0 +1,35 @@
/**
* @file This file provides a time menu class for editing the length of a test
* @author Arlo Filley
*/
/**
* This class provides the timer, which handles when a test starts and ends as well
* as providing a visual element for the user to see
*/
class UserShower {
constructor(x, y, height, width) {
this.button = new Button(x, y, height, width, "");
this.x = x;
this.y = y;
this.height = height;
this.width = width;
}
draw() {
textAlign(CENTER, CENTER);
this.button.draw();
imageMode(CENTER);
tint(255, 0, 255, 126);
image(accountIcon, this.x + this.height / 2, this.y + this.width / 2);
if (this.button.isPressed()) {
screenManager.setScreen(new AccountScreen());
}
}
}

27
public/index.css Executable file
View File

@ -0,0 +1,27 @@
/*
Index.css
Description: This file is the stylesheet for the html that the
user will see if they do not have javascript enabled.
Author: Arlo Filley
*/
:root {
--background-color: #dde83d;
--text-color: #000000;
--font: Verdana, Geneva, Tahoma, sans-serif;
}
body {
background-color: var(--background-color);
position: absolute;
margin: 0;
}
noscript {
display: block;
text-align: center;
font-family: var(--font);
color: var(--text-color);
font-size: large;
}

51
public/index.html Executable file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TypeFast</title>
<!-- CSS Files -->
<link rel="stylesheet" href="index.css">
<!-- Favicon Files -->
<link rel="apple-touch-icon" sizes="180x180" href="./assets/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon/favicon-16x16.png">
<link rel="manifest" href="./assets/favicon/site.webmanifest">
<!-- Main Script Files -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.min.js"></script> -->
<script src="./index.js" type="text/javascript"></script>
<!-- Element Script Files -->
<script src="./components/button.js" defer="true"></script>
<script src="./components/canvas.js" defer="true"></script>
<script src="./components/textbox.js" defer="true"></script>
<script src="./components/timemenu.js" defer="true"></script>
<script src="./components/timer.js" defer="true"></script>
<script src="./components/menu.js" defer="true"></script>
<script src="./components/user.js" defer="true"></script>
<!-- Screen Files-->
<script src="./screens/screenmanager.js" defer="true"></script>
<script src="./screens/startscreen.js" defer="true"></script>
<script src="./screens/testscreen.js" defer="true"></script>
<script src="./screens/endscreen.js" defer="true"></script>
<script src="./screens/accountScreen.js" defer="true"></script>
<script src="./screens/signUpScreen.js" defer="true"></script>
<script src="./screens/loginscreen.js" defer="true"></script>
<script src="./screens/profilescreen.js" defer="true"></script>
<script src="./screens/leaderboardscreen.js" defer="true"></script>
<script src="./screens/settingsScreen.js" defer="true"></script>
<!-- API Script Files -->
<script src="./api/api.js" defer="true"></script>
<script src="./api/user.js" defer="true"></script>
</head>
<body>
<noscript>Please Enable Javascript</noscript>
</body>
</html>

68
public/index.js Normal file
View File

@ -0,0 +1,68 @@
/**
* @file This files is the root of the website.
* @author Arlo Filley
*/
// these are all of the globally accessible variables that are
// needed for the site to run correctly
let canvas, api, screenManager, user;
/**
* loads the any assets before the setup function
* this allows p5.js to acess these assets including: sprites,
* fonts, etc
*/
function preload() {
roboto = loadFont('./assets/fonts/RobotoMono-Medium.ttf');
accountIcon = loadImage('./assets/icons/account_circle.svg');
}
/**
* defines variables and sets up the p5.js canvas
* ready to be drawn with using the draw() function
*/
function setup() {
canvas = new Canvas();
canvas.resize();
canvas.center();
frameRate(60);
api = new API();
screenManager = new ScreenManager();
user = new User();
screenManager.setScreen(new StartScreen());
api.login(null, null, true);
api.getTest();
textFont(roboto);
}
/**
* called once per frame. draws all other elements onto the canvas
* mostly will just call the screenManager.draw() method to make
* sure that the correct screen is being drawn
*/
function draw() {
background(user.colorScheme.background);
screenManager.draw();
}
/**
* called whenever a key is pressed, the variable key contains the
* key that the user last pressed
*/
function keyPressed() {
screenManager.letterTyped(key);
}
/**
* called whenever the user resizes the window. Uses methods from the canvas wrapper class
* to resize and center the canvas such that it displays correctly
*/
function windowResized() {
canvas.resize();
canvas.center();
}

115
public/screens/accountScreen.js Executable file
View File

@ -0,0 +1,115 @@
/**
* @file This file provides a screen for the user to login through
* @author Arlo Filley
*
* TODO:
* - move into an seperated account page with signup and logout
* - make passwords not display plain text
*/
/**
* this class displays a number of textboxes that allows the user to input a
* username and password. Then find out the user_id of that account through the
* necessary api routes.
*/
class AccountScreen {
constructor() {
this.textboxes = [
new Textbox(
120, 350, 500, 100, 0, true, "#000", false,
"#000", "#000", true
),
new Textbox(
120, 500, 500, 100, 0, true, "#000", false,
"#000", "#000", false, false, true
)
]
this.buttons = [
new Button(windowWidth / 2 - 400, 300, 500, 100, "", false, true, "#000", "#000", "#fff", "#000", "#000", "#fff"),
new Button(windowWidth / 2 - 400, 450, 500, 100, "", false, true, "#000", "#000", "#fff", "#000", "#000", "#fff"),
new Button(windowWidth / 2 + 200, 300, 100, 50, "Login"),
new Button(windowWidth / 2 + 200, 400, 100, 50, "Sign up"),
new Button(windowWidth / 2 + 200, 500, 100, 50, "Logout"),
]
this.menu = new Menu();
// keeps track of which textbox the user last clicked on
this.activeTextBox = 0
}
/**
* Draws the SignUpScreen class with all
* appropriate elements
*/
draw() {
textSize(100);
fill(user.colorScheme.text);
text("Account", 0, 100, windowWidth, 110);
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw();
}
for (let i = 0; i < this.textboxes.length; i++) {
this.textboxes[i].draw();
}
textSize(30);
text("Username", windowWidth / 2 - 400, 275);
text("Password", windowWidth / 2 - 400, 425);
if (this.buttons[0].isPressed()) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=0;
this.textboxes[this.activeTextBox].line = true;
} else if (this.buttons[1].isPressed()) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=1;
this.textboxes[this.activeTextBox].line = true;
} else if (this.buttons[2].isPressed()) {
api.login(
this.textboxes[0].getWords(),
this.textboxes[1].getWords()
)
screenManager.setScreen(new StartScreen());
} else if (this.buttons[3].isPressed()) {
api.createUser(
this.textboxes[0].getWords(),
this.textboxes[1].getWords()
)
screenManager.setScreen(new StartScreen());
} else if (this.buttons[4].isPressed()) {
api.logout();
screenManager.setScreen(new StartScreen());
}
this.menu.draw();
fill(user.colorScheme.text);
}
/**
*
* @param {key} key
*/
letterTyped(key) {
if (key === "Tab" && this.activeTextBox === 0) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=1;
this.textboxes[this.activeTextBox].line = true;
} else if (key === "Tab" && this.activeTextBox === 1) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=0;
this.textboxes[this.activeTextBox].line = true;
} else if (key === "Enter") {
api.login(
this.textboxes[0].getWords(),
this.textboxes[1].getWords()
)
screenManager.setScreen(new StartScreen());
} else {
this.textboxes[this.activeTextBox].letterTyped(key);
}
}
}

36
public/screens/endscreen.js Executable file
View File

@ -0,0 +1,36 @@
/**
* @file This file provides a screen class that can be displayed at the end of a test
* @author Arlo Filley
*
* TODO:
* - provide the user with the data of the test that they have just
* completed, such as their wpm, accuracy, etc.
*/
/**
* This class is for a screen that is displayed at the end of a test,
* currently it just tells the user to press start to enter another test
*/
class EndScreen {
constructor() {
this.menu = new Menu();
}
draw() {
textSize(100);
textAlign(CENTER, CENTER);
fill(user.colorScheme.text);
text("Test Complete", 0, 0, windowWidth - 100, windowHeight / 6);
textSize(30);
text(`${user.lastTest.wpm} words per minute`, windowWidth / 2, 200);
text(`${user.lastTest.accuracy}% accuracy`, windowWidth / 2, 240);
text(`${user.lastTest.test_length} characters typed`, windowWidth / 2, 280);
text(`${user.lastTest.test_time}s`, windowWidth / 2, 320);
this.menu.draw();
}
letterTyped(key) {
if (key === "Enter") screenManager.setScreen(new TestScreen());
}
}

View File

@ -0,0 +1,101 @@
/**
* @file This file provides a leaderboard for the user to compare times.
* @author Arlo Filley
*
* TODO:
* - implement a way for the user to scroll down the tests.
* - display more tests on the screen at once, up to 15
* - store the leaderboard in localstorage as a cache for the most recent results
*/
/**
* this class is a screen which shows the current leaderboard from the
* results gotten via the api.
*/
class LeaderboardScreen {
constructor() {
this.menu = new Menu();
api.getLeaderBoard();
this.testButtons;
// this.buttons = [
// new Button(1150, 270, 240, 120, "up"),
// new Button(1150, 390, 240, 120, "down"),
// ]
this.offset = 0;
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
}
draw() {
textSize(100);
textAlign(CENTER, CENTER);
fill(user.colorScheme.text);
text("Leaderboard", 0, 100, windowWidth, 120);
this.menu.draw();
textSize(20);
fill(user.colorScheme.text);
if (user.leaderboard != undefined) {
if (this.testButtons === undefined) {
this.createTestButtons();
}
}
fill(user.colorScheme.testBad);
rect(1200, 270, 20, 420);
fill(user.colorScheme.testGood);
rect(1200, 270, 20, 420 / user.leaderboard.length * (this.offset + 1));
this.scroll_bar_button.height = (user.leaderboard.length)
if (this.scroll_bar_button.isPressed()) {
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
}
// the furthest up the scrollbar can go is the top
if (this.scroll_bar_button.y < 270) {
this.scroll_bar_button.y = 270;
}
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
}
this.scroll_bar_button.draw();
if (this.testButtons !== undefined && this.testButtons.length > 1) {
for (let i = 0; i < this.testButtons.length; i++) {
this.testButtons[i][0].draw()
this.testButtons[i][1].draw()
this.testButtons[i][2].draw()
}
this.offset = Number((
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.leaderboard.length - 13))
).toFixed(0));
this.createTestButtons(this.offset)
} else {
fill(user.colorScheme.text);
text("Looks Like There Isn't A Leaderboard", windowWidth / 2, 300);
}
fill(user.colorScheme.text);
}
createTestButtons(offset = 0) {
this.testButtons = [[
new Button(400, 270, 100, 30, "ranking"), // test # button
new Button(500, 270, 400, 30, "username"), // wpm button
new Button(900, 270, 240, 30, "words per minute"), // accuracy button
]];
let j = 300;
for (let i = 0 + offset; i < user.leaderboard.length && i <= 12+offset; i++) {
this.testButtons.push([
new Button(400, j, 100, 30, `${i+1}`, true, true, "#000", "#000", "#fff"), // test # button
new Button(500, j, 400, 30, `${user.leaderboard[i].username}`, true, true, "#000", "#000", "#fff"), // accuracy button
new Button(900, j, 240, 30, `${user.leaderboard[i].wpm}`, true, true, "#000", "#000", "#fff"), // wpm button
])
j+=30;
}
}
}

108
public/screens/loginscreen.js Executable file
View File

@ -0,0 +1,108 @@
/**
* @file This file provides a screen for the user to login through
* @author Arlo Filley
*
* TODO:
* - move into an seperated account page with signup and logout
* - make passwords not display plain text
*/
/**
* this class displays a number of textboxes that allows the user to input a
* username and password. Then find out the user_id of that account through the
* necessary api routes.
*/
class LoginScreen {
constructor() {
this.textboxes = [
new Textbox(
120, 250, 500, 100, 0, true, user.colorScheme.text, false,
"#000", "#000", true, false
),
new Textbox(
120, 400, 500, 100, 0, true, user.colorScheme.text, false,
"000", "#000", false, false
)
]
this.buttons = [
new Button(
100, 200, 500, 100, 0, true, user.colorScheme.buttonBG, false,
"#000", "#fff", ""
),
new Button(
100, 350, 500, 100, 0, true, "#000", false,
"#000", "#fff", ""
),
new Button(
700, 300, 100, 50, 0, true, "#000", false,
"#000", "#00ff00", "Login"
),
]
this.menu = new Menu();
// keeps track of which textbox the user last clicked on
this.activeTextBox = 0
}
/**
* Draws the SignUpScreen class with all
* appropriate elements
*/
draw() {
fill(user.colorScheme.text);
text("Username", 110, 175);
text("Password", 110, 325);
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw();
}
for (let i = 0; i < this.textboxes.length; i++) {
this.textboxes[i].draw();
}
if (this.buttons[0].isPressed()) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=0;
this.textboxes[this.activeTextBox].line = true;
} else if (this.buttons[1].isPressed()) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=1;
this.textboxes[this.activeTextBox].line = true;
} else if (this.buttons[2].isPressed()) {
api.login(this.textboxes[0].getWords(), this.textboxes[1].getWords())
screenManager.setScreen(new StartScreen());
}
this.menu.draw();
}
/**
*
* @param {key} key
*/
letterTyped(key) {
if (key === "Tab" && this.activeTextBox === 0) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox = 1;
this.textboxes[this.activeTextBox].line = true;
} else if (key === "Tab" && this.activeTextBox === 1) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox = 0;
this.textboxes[this.activeTextBox].line = true;
} else if (key === "Enter") {
api.login(
this.textboxes[0].getWords(),
this.textboxes[1].getWords()
)
screenManager.setScreen(new StartScreen());
} else {
this.textboxes[this.activeTextBox].letterTyped(key);
}
}
}

111
public/screens/profilescreen.js Executable file
View File

@ -0,0 +1,111 @@
/**
* @file This file provides the user with their profilescreen, where they can see their own tests
* @author Arlo Filley
*
* TODO:
* - change button name
* - provide filters for tests
* - implement a way to scroll through tests
* - create a way to have personal bests and track them
* - store tests in localstorage.
* - show user tests even if they are not logged in
*/
/**
* This class displays all of the test data for a given user
*/
class ProfileScreen {
constructor() {
this.menu = new Menu();
api.getUserTests();
this.testButtons;
// this.buttons = [
// new Button(950, 270, 240, 120, "up"),
// new Button(950, 390, 240, 120, "down"),
// ]
this.offset = 0;
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
}
draw() {
textSize(100);
textAlign(CENTER, CENTER);
fill(user.colorScheme.text);
text("Profile", 0, 100, windowWidth, 120);
this.menu.draw();
textSize(20);
fill(user.colorScheme.text);
if (user.tests != undefined) {
if (this.testButtons === undefined) {
this.createTestButtons();
}
}
if (this.testButtons !== undefined && this.testButtons.length > 1) {
for (let i = 0; i < this.testButtons.length; i++) {
this.testButtons[i][0].draw()
this.testButtons[i][1].draw()
this.testButtons[i][2].draw()
this.testButtons[i][3].draw()
}
} else {
fill(user.colorScheme.text);
text("Looks Like You Don't have any tests :(", windowWidth / 2, 300);
}
fill(user.colorScheme.text);
if (user.tests === undefined) {
return;
}
fill(user.colorScheme.testBad);
rect(1200, 270, 20, 420);
fill(user.colorScheme.testGood);
rect(1200, 270, 20, 420 / user.tests.length * (this.offset + 1));
this.scroll_bar_button.height = (user.tests.length)
if (this.scroll_bar_button.isPressed()) {
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
}
// the furthest up the scrollbar can go is the top
if (this.scroll_bar_button.y < 270) {
this.scroll_bar_button.y = 270;
}
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
}
this.scroll_bar_button.draw();
this.offset = Number((
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.tests.length - 13))
).toFixed(0));
this.createTestButtons(this.offset);
}
createTestButtons(offset = 0) {
this.testButtons = [[
new Button(600, 270, 100, 30, "test #"), // test # button
new Button(700, 270, 100, 30, "wpm"), // wpm button
new Button(800, 270, 100, 30, "accuracy"), // accuracy button
new Button(900, 270, 240, 30, "characters typed")
]];
let j = 300;
for (let i = user.tests.length-1-offset; i >= user.tests.length-13-offset && i >= 0; i--) {
this.testButtons.push([
new Button(600, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button
new Button(700, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button
new Button(800, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button
new Button(900, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff")
])
j+=30;
}
}
}

41
public/screens/screenmanager.js Executable file
View File

@ -0,0 +1,41 @@
/**
* @file This file provides the screen manager class, with the necassary code to switch between screen classes
* @author Arlo Filley
*
* TODO:
* - implement transitions between screens in a more fluid way
*/
/**
* This class provides the ScreenManager class stores the current screen
* and provides the getters and setters necessary to switch between screen classes
* easily
*/
class ScreenManager {
constructor() {
this.textbox;
this.timer;
this.screen;
}
draw() {
this.screen.draw();
}
setScreen(pScreen) {
this.screen = pScreen;
}
getScreen() {
return this.screen;
}
letterTyped(key) {
let methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this.screen));
for (let i = 0; i < methods.length; i++) {
if (methods[i] === "letterTyped") {
this.screen.letterTyped(key)
}
}
}
}

View File

@ -0,0 +1,29 @@
/**
* @file This file provides a screen where the user can edit the settings of their tests
* @author Arlo Filley
*/
/**
* This class provides all of the necessary settings for the user to be able to edit test settings
*/
class SettingsScreen {
constructor() {
this.menu = new Menu();
this.timeMenu = new TimeMenu();
}
draw() {
textAlign(CENTER, CENTER);
textSize(100);
fill(user.colorScheme.text);
text("Test Settings", 0, 100, windowWidth, 110);
this.menu.draw();
fill(user.colorScheme.text);
text("Test Duration", windowWidth / 2 - 250, 265)
this.timeMenu.draw();
fill("#000");
}
}

109
public/screens/signUpScreen.js Executable file
View File

@ -0,0 +1,109 @@
/**
* @file This file provides a way for the user to sign up for an account
* @author Arlo Filley
*
* TODO:
* - move into an seperated account page with signup and logout
* - make passwords not display plain text
*/
/**
* This class provides the textboxes and methods necessary for a user
* to sign up for a new account, which it should then log them into
*/
class SignUpScreen {
constructor() {
this.textboxes = [
new Textbox(
120, 250, 500, 100,0, true, "#000", false,
"#000", "#000", true
),
new Textbox(
120, 400, 500, 100, 0, true, "#000", false,
"000", "#000", false
)
]
this.buttons = [
new Button(
100, 200, 500, 100, 0, true, "#000", false,
"#000", "#fff", ""
),
new Button(
100, 350, 500, 100, 0, true, "#000", false,
"#000", "#fff", ""
),
new Button(
700, 300, 100, 50, 0, true, "#000", false,
"#000", "#00ff00", "Sign Up"
),
]
this.menu = new Menu();
this.activeTextBox = 0
// keeps track of which textbox the user last clicked on
}
/**
* Draws the SignUpScreen class with all
* appropriate elements
*/
draw() {
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw();
}
for (let i = 0; i < this.textboxes.length; i++) {
this.textboxes[i].draw();
}
fill(user.colorScheme.text);
text("Username", 110, 175);
text("Password", 110, 325);
if (this.buttons[0].isPressed()) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=0;
this.textboxes[this.activeTextBox].line = true;
} else if (this.buttons[1].isPressed()) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=1;
this.textboxes[this.activeTextBox].line = true;
} else if (this.buttons[2].isPressed()) {
api.createUser(
this.textboxes[0].getWords(),
this.textboxes[1].getWords()
)
screenManager.setScreen(new StartScreen());
}
this.menu.draw();
}
/**
*
* @param {key} key
*/
letterTyped(key) {
if (key === "Tab" && this.activeTextBox === 0) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=1;
this.textboxes[this.activeTextBox].line = true;
} else if (key === "Tab" && this.activeTextBox === 1) {
this.textboxes[this.activeTextBox].line = false;
this.activeTextBox=0;
this.textboxes[this.activeTextBox].line = true;
} else if (key === "Enter") {
api.createUser(
this.textboxes[0].getWords(),
this.textboxes[1].getWords()
)
screenManager.setScreen(new StartScreen());
} else {
this.textboxes[this.activeTextBox].letterTyped(key);
}
}
}

31
public/screens/startscreen.js Executable file
View File

@ -0,0 +1,31 @@
/**
* @file This file is the base screen when the user visits the site
* @author Arlo Filley
*/
/**
* This screen class is the base screen. It provides the user with basic instructions
* and a set of menus to navigate the site
*/
class StartScreen {
constructor() {
this.menu = new Menu();
}
draw() {
textSize(100);
textAlign(CENTER, CENTER);
fill(user.colorScheme.text);
text("Press enter to start test", 0, 0, windowWidth, windowHeight);
this.menu.draw();
fill(user.colorScheme.text);
}
letterTyped(key) {
if (key === "Enter") {
screenManager.setScreen(new TestScreen());
}
}
}

45
public/screens/testscreen.js Executable file
View File

@ -0,0 +1,45 @@
/**
* @file This file provides the functionality for the test
* @author Arlo Filley
*
* TODO:
* - provide a button that allows the user to exit the test
* - provide a count down to the start of a test
* - implement menus to allow the user to control the parameters of a test
*/
/**
* this class displays the text of the test to the the screen and then takes input from the user
* displaying red if it is inaccurate, and green if it is
*/
class TestScreen {
constructor() {
this.textbox = new Textbox(
100, windowHeight / 2 - 100,
windowWidth - 500,windowHeight,
0, true, "#000", false, "#000", "#000", true, true);
this.timer = new Timer(0,0,windowWidth,50,0,true,"#fff", true, "#000", "#666", user.time, true);
this.timerStarted = false;
this.stopButton = new Button(0,50,200,50, "Stop Test");
}
draw() {
this.textbox.draw();
this.timer.draw();
if (this.timerStarted) {
this.timer.tick();
}
this.stopButton.draw();
if (this.stopButton.isPressed()) {
screenManager.setScreen(new StartScreen())
}
}
letterTyped(key) {
this.textbox.letterTyped(key);
if (!this.timerStarted) {
this.timer.start();
this.timerStarted = true;
}
}
}

2
scripts/build.sh Normal file
View File

@ -0,0 +1,2 @@
#/bin/sh
docker build -t "typing-website:Dockerfile" .

2
scripts/run.sh Executable file
View File

@ -0,0 +1,2 @@
#/bin/bash
docker run -p 8000:8000 -v /Users/arlo/Code/CS-Coursework/database/dev/database.sqlite:/app/database/dev/database.sqlite -v /Users/arlo/Code/CS-Coursework/public:/app/public "typing-website:Dockerfile" \

View File

@ -10,27 +10,27 @@ use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Request, Response};
pub struct CORS;
// pub struct CORS;
#[rocket::async_trait]
impl Fairing for CORS {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to responses",
kind: Kind::Response,
}
}
// #[rocket::async_trait]
// impl Fairing for CORS {
// fn info(&self) -> Info {
// Info {
// name: "Add CORS headers to responses",
// kind: Kind::Response,
// }
// }
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new(
"Access-Control-Allow-Methods",
"POST, GET, PATCH, OPTIONS",
));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}
// async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
// response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
// response.set_header(Header::new(
// "Access-Control-Allow-Methods",
// "POST, GET, PATCH, OPTIONS",
// ));
// response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
// response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
// }
// }
// Imports for rocket
#[macro_use]
@ -63,7 +63,7 @@ fn test() -> &'static str {
#[launch]
async fn rocket() -> Rocket<Build> {
rocket::build()
.attach(CORS)
// .attach(CORS)
// testing only, should return "Hello world"
.mount("/test", routes![test])
// hosts the api routes necessary for the website

View File

@ -10,8 +10,10 @@
//! - create structure for the input of post test
// Imports for json handling and rusqlite
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use rocket::serde::Serialize;
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
use rocket::serde::{ Serialize, json::Json };
use crate::typing::test::PostTest;
/// Contains the database connection pool
pub struct Database(SqlitePool);
@ -60,11 +62,23 @@ impl Database {
/// takes necessary data about a test and creates
/// a database record with the data
pub async fn create_test(&self, test_type: &str, test_length: u32, test_time: u32, test_seed: i64, quote_id: i32, wpm: u8, accuracy: u8, user_id: u32) -> Result<(), sqlx::Error> {
pub async fn create_test(&self, test: Json<PostTest<'_>>) -> Result<(), sqlx::Error> {
// Test to see whether the secret is correct
let user = sqlx::query!("
Select secret
From Users
Where user_id=?",
test.user_id
).fetch_one(&self.0).await?;
if user.secret != test.secret {
return Err(sqlx::Error::RowNotFound)
}
sqlx::query!("
INSERT INTO Tests (test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id
test.test_type, test.test_length, test.test_time, test.test_seed, test.quote_id, test.wpm, test.accuracy, test.user_id
).execute(&self.0).await?;
Ok(())

View File

@ -17,23 +17,25 @@ use std::{
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct PostTest<'r> {
test_type: &'r str,
test_length: u32,
test_time: u32,
test_seed: i64,
quote_id: i32,
wpm: u8,
accuracy: u8,
user_id: u32
pub test_type: &'r str,
pub test_length: u32,
pub test_time: u32,
pub test_seed: i64,
pub quote_id: i32,
pub wpm: u8,
pub accuracy: u8,
pub user_id: u32,
pub secret: &'r str
}
/// Api Route that accepts test data and posts it to the database
/// Acessible from http://url/api/post_test
#[post("/post_test", data = "<test>")]
pub async fn create_test(test: Json<PostTest<'_>>, database: &State<Database>) {
match database.create_test(test.test_type, test.test_length, test.test_time, test.test_seed, test.quote_id, test.wpm, test.accuracy, test.user_id).await {
let user_id = test.user_id;
match database.create_test(test).await {
Err(why) => { println!("A database error occured creating a test, {why}"); }
Ok(()) => { println!("Successfully created test for {}", test.user_id); }
Ok(()) => { println!("Successfully created test for {user_id}"); }
}
}

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<link rel="stylesheet" href="../index.css">
<script src="index.js"></script>
</head>
<body>
<nav>
<p>Arlo Filley</p>
<ul>
<li><a href="../">Home</a></li>
<li><a href="./">Delete A User</a></li>
</ul>
</nav>
<h1>Password</h1>
<input id="password" type="password">
<table>
<tbody id="table"></tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,51 @@
get();
function get() {
let xhr = new XMLHttpRequest();
xhr.open('GET', `https://arlofilley.com/api/typing/leaderboard`);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.send();
xhr.onload = () => {
json = xhr.response
createTable(json);
};
}
function createTable(pJson) {
let table = document.getElementById("table");
console.log(password)
pJson.forEach(element => {
let tr = document.createElement('tr');
let button = document.createElement('button');
let username = element.username;
button.textContent = "delete"
button.addEventListener( "click", () => {
let xhr = new XMLHttpRequest();
let password = document.getElementById("password").value;
xhr.open('GET', `https://arlofilley.com/api/typing/delete_user/${password}/${username}/120932187`);
xhr.send();
})
create_table_element(tr, "", [element.username, element.wpm], button)
table.appendChild(tr)
});
}
function create_table_element(tr, string, elements, button) {
if (elements.length > 0) {
for (let i = 0; i < elements.length; i++) {
let td = document.createElement('td');
td.appendChild(document.createTextNode(elements[i]));
tr.appendChild(td);
}
} else {
let td = document.createElement('td');
td.appendChild(document.createTextNode(string));
tr.appendChild(td);
}
tr.appendChild(button);
}

125
websites/Typing/Admin/index.css Executable file
View File

@ -0,0 +1,125 @@
:root {
--navbar: #333333;
/* Navbar Button Properties */
--navbar-button: #058ED9;
--navbar-button-hover: #283F3B;
--navbar-button-border: #058ED9;
/* */
--background: #1f1f1f;
/* Table Properties*/
--table: #FFD07B;
--table-top:#FDB833;
/* Title Properties */
--title-background: #333333;
--title-text: #ffffff;
/* General Properties */
--text: #000000;
--border: #000000;
}
body {
background-color: #111;
margin: 0;
padding: 0;
}
nav {
background-color: #333;
height: 75px;
display: flex;
justify-content: center;
align-items: center;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
nav p {
margin-right: 700px;
text-decoration: none;
color: #fff;
font-size: 38px;
font-weight: bold;
text-transform: uppercase;
font-family: "Montserrat", sans-serif;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
display: inline-block;
margin: 0 20px;
}
a {
text-decoration: none;
color: #fff;
font-size: 18px;
font-weight: bold;
text-transform: uppercase;
font-family: "Montserrat", sans-serif;
padding: 10px 20px;
transition: background-color 0.3s ease;
}
a:hover {
background-color: #555;
}
h1 {
margin-left: 5%;
margin-bottom: 0%;
color: #fff;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
input {
margin-left: 5%;
margin-top: 1.5%;
}
table {
width: 90%;
border-collapse: collapse;
margin: 0px;
margin-left: 5%;
margin-top: 30px;
padding: 0px;
border-spacing: 0px;
border: 2px var(--border) solid;
border-radius: 20px;
}
table tr:nth-child(n) {
background: var(--table);
}
table tr:nth-child(1) {
background: var(--table-top);
}
tr {
border: 1px black dotted;
}
td {
text-align: center;
padding: 10px;
font-size: larger;
}
button {
margin-left: 40%;
padding: 8px;
padding-left: 12.5%;
padding-right: 12.5%;
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<nav>
<p>Arlo Filley</p>
<ul>
<li><a href="./">Home</a></li>
<li><a href="./Delete User/">Delete A User</a></li>
</ul>
</nav>
</body>
</html>

View File

@ -15,12 +15,7 @@
*/
class API {
constructor() {
// this.url = "https://arlofilley.com/api/";
this.url = "../api";
// this is the url of the server
// this may have to change later on
}
constructor() { this.url = "../api"; }
/**
* This takes the validated data and makes a post
@ -43,7 +38,8 @@ class API {
'quote_id': pQuoteId,
'wpm': pWpm,
'accuracy': pAccuracy,
'user_id': pUserId
'user_id': pUserId,
'secret': user.secret
}
const xhr = new XMLHttpRequest();
@ -210,13 +206,8 @@ class API {
* @returns
*/
login(pUsername, pPassword, initial = false) {
// Variable Validation
if (pUsername == undefined || pPassword == undefined) {
return
}
// If Local Storage has the information we need there is no need to make a request to the server
if (localStorage.getItem("username") == pUsername) {
if (localStorage.getItem("username") === pUsername || (initial && localStorage.length === 3) ) {
user.userId = localStorage.getItem("userId");
user.secret = localStorage.getItem("secret");
user.username = localStorage.getItem("username");
@ -224,6 +215,11 @@ class API {
return
}
// Variable Validation
if (pUsername == undefined || pPassword == undefined) {
return
}
let xhr = new XMLHttpRequest();
xhr.open('GET', `${this.url}/login/${pUsername}/${pPassword}`);
xhr.send();
@ -231,7 +227,7 @@ class API {
let response = JSON.parse(xhr.response);
// If there is an error with the login we need
if (xhr.response == null) {
if (xhr.response === null) {
alert("Error Logging in, maybe check your password");
return
}
@ -249,7 +245,7 @@ class API {
logout() {
user = new User();
user.username = "no one";
user.password = "none";
user.password = "";
user.userId = 0;
user.tests = [];
localStorage.clear();

8
websites/Typing/api/user.js Normal file → Executable file
View File

@ -17,7 +17,7 @@ class User {
constructor() {
this.username = "not logged in";
this.userId = 0;
this.secret = "";
this.secret;
this.leaderboard;
this.time = 15;
@ -27,8 +27,8 @@ class User {
this.nextTest = `satisfy powerful pleasant bells disastrous mean kited is gusted romantic past taste immolate productive leak close show crabby awake handsails finicky betray long-term incompetent wander show manage toys convey hop constitute number send like off ice aboard well-made vast vacuous tramp seed force divergent flower porter fire untidy soggy fetch`;
this.colorScheme = {
background: "#eeeee4",
text: "#000",
background: "#160C1C",
text: "#FBB",
timerBar: "#50C5B7",
timerText: "#000",
@ -40,7 +40,7 @@ class User {
buttonText: "#fff",
buttonBorder: "#000",
buttonHoverBG: "#0f0",
buttonHoverBG: "#FBB",
buttonHoverText: "#000",
buttonHoverBorder: "#000"
}

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

0
websites/Typing/assets/favicon/apple-touch-icon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

0
websites/Typing/assets/favicon/favicon-16x16.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 552 B

0
websites/Typing/assets/favicon/favicon-32x32.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 968 B

0
websites/Typing/assets/favicon/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

0
websites/Typing/assets/favicon/site.webmanifest Normal file → Executable file
View File

0
websites/Typing/assets/fonts/RobotoMono-Medium.ttf Normal file → Executable file
View File

0
websites/Typing/index.css Normal file → Executable file
View File

0
websites/Typing/index.html Normal file → Executable file
View File

View File

@ -32,7 +32,7 @@ function setup() {
user = new User();
screenManager.setScreen(new StartScreen());
api.login();
api.login(null, null, true);
api.getTest();
textFont(roboto);
}

0
websites/Typing/lib/p5.js Normal file → Executable file
View File

0
websites/Typing/lib/p5.min.js vendored Normal file → Executable file
View File

0
websites/Typing/screens/accountScreen.js Normal file → Executable file
View File

0
websites/Typing/screens/endscreen.js Normal file → Executable file
View File

51
websites/Typing/screens/leaderboardscreen.js Normal file → Executable file
View File

@ -17,11 +17,12 @@ class LeaderboardScreen {
this.menu = new Menu();
api.getLeaderBoard();
this.testButtons;
this.buttons = [
new Button(1150, 270, 240, 120, "up"),
new Button(1150, 390, 240, 120, "down"),
]
// this.buttons = [
// new Button(1150, 270, 240, 120, "up"),
// new Button(1150, 390, 240, 120, "down"),
// ]
this.offset = 0;
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
}
draw() {
@ -39,29 +40,40 @@ class LeaderboardScreen {
}
}
fill(user.colorScheme.testBad);
rect(1200, 270, 20, 420);
fill(user.colorScheme.testGood);
rect(1200, 270, 20, 420 / user.leaderboard.length * (this.offset + 1));
this.scroll_bar_button.height = (user.leaderboard.length)
if (this.scroll_bar_button.isPressed()) {
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
}
// the furthest up the scrollbar can go is the top
if (this.scroll_bar_button.y < 270) {
this.scroll_bar_button.y = 270;
}
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
}
this.scroll_bar_button.draw();
if (this.testButtons !== undefined && this.testButtons.length > 1) {
for (let i = 0; i < this.testButtons.length; i++) {
this.testButtons[i][0].draw()
this.testButtons[i][1].draw()
this.testButtons[i][2].draw()
}
if (this.buttons[0].isPressed()) {
if (this.offset > 0) {
this.offset--;
}
this.createTestButtons(this.offset);
} else if (this.buttons[1].isPressed()) {
if (this.offset < user.leaderboard.length - 13) {
this.offset++;
}
this.createTestButtons(this.offset);
}
this.offset = Number((
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.leaderboard.length - 13))
).toFixed(0));
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw();
}
this.createTestButtons(this.offset)
} else {
fill(user.colorScheme.text);
text("Looks Like There Isn't A Leaderboard", windowWidth / 2, 300);
@ -78,7 +90,6 @@ class LeaderboardScreen {
]];
let j = 300;
for (let i = 0 + offset; i < user.leaderboard.length && i <= 12+offset; i++) {
console.log(i);
this.testButtons.push([
new Button(400, j, 100, 30, `${i+1}`, true, true, "#000", "#000", "#fff"), // test # button
new Button(500, j, 400, 30, `${user.leaderboard[i].username}`, true, true, "#000", "#000", "#fff"), // accuracy button

0
websites/Typing/screens/loginscreen.js Normal file → Executable file
View File

72
websites/Typing/screens/profilescreen.js Normal file → Executable file
View File

@ -19,11 +19,12 @@ class ProfileScreen {
this.menu = new Menu();
api.getUserTests();
this.testButtons;
this.buttons = [
new Button(950, 270, 240, 120, "up"),
new Button(950, 390, 240, 120, "down"),
]
// this.buttons = [
// new Button(950, 270, 240, 120, "up"),
// new Button(950, 390, 240, 120, "down"),
// ]
this.offset = 0;
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
}
draw() {
@ -49,21 +50,6 @@ class ProfileScreen {
this.testButtons[i][2].draw()
this.testButtons[i][3].draw()
}
if (this.buttons[0].isPressed()) {
if (this.offset > 0) {
this.offset--;
}
this.createTestButtons(this.offset);
} else if (this.buttons[1].isPressed()) {
if (user.tests.length - 13 - this.offset > 0) {
this.offset++;
}
this.createTestButtons(this.offset);
}
for (let i = 0; i < this.buttons.length; i++) {
this.buttons[i].draw();
}
} else {
fill(user.colorScheme.text);
text("Looks Like You Don't have any tests :(", windowWidth / 2, 300);
@ -71,22 +57,54 @@ class ProfileScreen {
fill(user.colorScheme.text);
text(`Logged in as ${user.username}`, windowWidth-150, 15);
if (user.tests === undefined) {
return;
}
fill(user.colorScheme.testBad);
rect(1200, 270, 20, 420);
fill(user.colorScheme.testGood);
rect(1200, 270, 20, 420 / user.tests.length * (this.offset + 1));
this.scroll_bar_button.height = (user.tests.length)
if (this.scroll_bar_button.isPressed()) {
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
}
// the furthest up the scrollbar can go is the top
if (this.scroll_bar_button.y < 270) {
this.scroll_bar_button.y = 270;
}
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
}
this.scroll_bar_button.draw();
this.offset = Number((
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.tests.length - 13))
).toFixed(0));
this.createTestButtons(this.offset);
}
createTestButtons(offset = 0) {
this.testButtons = [[
new Button(400, 270, 100, 30, "test #"), // test # button
new Button(500, 270, 100, 30, "wpm"), // wpm button
new Button(600, 270, 100, 30, "accuracy"), // accuracy button
new Button(700, 270, 240, 30, "characters typed")
new Button(600, 270, 100, 30, "test #"), // test # button
new Button(700, 270, 100, 30, "wpm"), // wpm button
new Button(800, 270, 100, 30, "accuracy"), // accuracy button
new Button(900, 270, 240, 30, "characters typed")
]];
let j = 300;
for (let i = user.tests.length-1-offset; i >= user.tests.length-13-offset && i >= 0; i--) {
this.testButtons.push([
new Button(400, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button
new Button(500, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button
new Button(600, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button
new Button(700, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff")
new Button(600, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button
new Button(700, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button
new Button(800, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button
new Button(900, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff")
])
j+=30;
}

0
websites/Typing/screens/screenmanager.js Normal file → Executable file
View File

5
websites/Typing/screens/settingsScreen.js Normal file → Executable file
View File

@ -17,9 +17,12 @@ class settingsScreen {
textSize(100);
fill(user.colorScheme.text);
text("Test Settings", 450, 100);
text("Test Settings", windowWidth / 2, 100);
this.menu.draw();
fill(user.colorScheme.text);
text("Test Duration", windowWidth / 2 - 250, 265)
this.timeMenu.draw();
fill("#000");
text(`Logged in as ${user.username}`, windowWidth-150, 15);

0
websites/Typing/screens/signUpScreen.js Normal file → Executable file
View File

0
websites/Typing/screens/startscreen.js Normal file → Executable file
View File

10
websites/Typing/screens/testscreen.js Normal file → Executable file
View File

@ -14,9 +14,13 @@
*/
class TestScreen {
constructor() {
this.textbox = new Textbox(100,100,windowWidth - 200,windowHeight,0,true,"#000", false, "#000", "#000", true, true);
this.textbox = new Textbox(
100, windowHeight / 2 - 100,
windowWidth - 500,windowHeight,
0, true, "#000", false, "#000", "#000", true, true);
this.timer = new Timer(0,0,windowWidth,50,0,true,"#fff", true, "#000", "#666", user.time, true);
this.timerStarted = false;
this.stopButton = new Button(0,50,200,50, "Stop Test");
}
draw() {
@ -25,6 +29,10 @@ class TestScreen {
if (this.timerStarted) {
this.timer.tick();
}
this.stopButton.draw();
if (this.stopButton.isPressed()) {
screenManager.setScreen(new StartScreen())
}
}
letterTyped(key) {

1
websites/Typing/ui_elements/button.js Normal file → Executable file
View File

@ -179,6 +179,7 @@ class Button {
return;
}
textSize(20);
textAlign(CENTER, CENTER);
if (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height) {

0
websites/Typing/ui_elements/canvas.js Normal file → Executable file
View File

10
websites/Typing/ui_elements/menu.js Normal file → Executable file
View File

@ -13,11 +13,11 @@
class Menu {
constructor() {
this.buttons = [
new Button(0, 0, 100, 30, "Account"),
new Button(101, 0, 130, 30, "Test Data"),
new Button(232, 0, 140, 30, "Start Test"),
new Button(373, 0, 140, 30, "Leaderboard"),
new Button(514, 0, 180, 30, "Test Settings")
new Button(0, 0, 200, 50, "Account"),
new Button(201, 0, 200, 50, "Test Data"),
new Button(402, 0, 200, 50, "Start Test"),
new Button(603, 0, 200, 50, "Leaderboard"),
new Button(804, 0, 200, 50, "Test Settings")
]
}

20
websites/Typing/ui_elements/textbox.js Normal file → Executable file
View File

@ -27,6 +27,9 @@ class Textbox {
* @param {bool} pBorder
* @param {hexcode} pBorderColor
* @param {hexcode} pBackgroundColor
* @param {bool} pLine
* @param {bool} pIsTest
* @param {bool} pIsPassword
*/
constructor(
pX, pY,
@ -73,6 +76,7 @@ class Textbox {
this.goodColor = user.colorScheme.testGood;
this.badColor = user.colorScheme.testBad;
this.textColor = user.colorScheme.text;
}
getX() {
@ -271,7 +275,9 @@ class Textbox {
} else {
fill("#00044");
}
text(this.testContent[this.currentLine-1][x], i, j);
i += 13;
}
j+= 30;
@ -289,10 +295,18 @@ class Textbox {
} else {
fill(this.textColor);
}
text(this.testContent[this.currentLine][x], i, j);
if (this.letters.length > 0 && x == this.letters.length && this.line) {
fill(this.textColor)
rect(i, j-15, 1, 30)
}
i += 13;
}
i = this.x;
j += 30;
@ -300,7 +314,7 @@ class Textbox {
for (let x = this.currentLine + 1; x < this.testContent.length; x++) {
text(this.testContent[x], i, j);
j += 30;
}
}
} else if (this.isPassword) {
// these variables allow me to use the values of x and y while updating them
@ -343,4 +357,8 @@ class Textbox {
}
}
}
draw_line(line, y) {
}
}

24
websites/Typing/ui_elements/timemenu.js Normal file → Executable file
View File

@ -15,13 +15,14 @@
class TimeMenu {
constructor() {
this.buttons = [
new Button(100, 230, 100, 30, "15s"),
new Button(100, 260, 100, 30, "30s"),
new Button(100, 290, 100, 30, "45s"),
new Button(100, 320, 100, 30, "60s"),
new Button(900, 250, 100, 30, "15s"),
new Button(900, 280, 100, 30, "30s"),
new Button(900, 310, 100, 30, "45s"),
new Button(900, 340, 100, 30, "60s"),
];
this.topButton = this.buttons[0];
this.dropDownButton = new Button(1000, 250, 30, 30, "v")
this.dropdown = false;
}
@ -33,29 +34,28 @@ class TimeMenu {
if (this.buttons[0].isPressed() && user.time != 15) {
user.time = 15;
this.topButton = new Button(100, 230, 100, 30, "15s");
this.topButton = new Button(900, 250, 100, 30, "15s");
this.dropdown = false;
} else if (this.buttons[1].isPressed()) {
user.time = 30;
this.topButton = new Button(100, 230, 100, 30, "30s");
this.topButton = new Button(900, 250, 100, 30, "30s");
this.dropdown = false;
} else if (this.buttons[2].isPressed()) {
user.time = 45;
this.topButton = new Button(100, 230, 100, 30, "45s");
this.topButton = new Button(900, 250, 100, 30, "45s");
this.dropdown = false;
} else if (this.buttons[3].isPressed()) {
user.time = 60;
this.topButton = new Button(100, 230, 100, 30, "60s");
this.topButton = new Button(900, 250, 100, 30, "60s");
this.dropdown = false;
}
} else {
this.topButton.draw();
}
if (this.topButton.isPressed()) {
this.dropDownButton.draw();
if (this.dropDownButton.isPressed()) {
this.dropdown = true;
} else if (mouseIsPressed) {
this.dropdown = false;
}
} else if (mouseIsPressed) { this.dropdown = false };
}
}

0
websites/Typing/ui_elements/timer.js Normal file → Executable file
View File