open_evening_2

This commit is contained in:
Arlo Filley 2023-10-17 15:15:41 +01:00
parent 3aec4a052f
commit 90bb99f232
43 changed files with 2507 additions and 20 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

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