diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ab8ebce
--- /dev/null
+++ b/Dockerfile
@@ -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" ]
\ No newline at end of file
diff --git a/Rocket.toml b/Rocket.toml
new file mode 100644
index 0000000..150fdd6
--- /dev/null
+++ b/Rocket.toml
@@ -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
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index 10cfcd8..93f3692 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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
\ No newline at end of file
+-[ ] Be able to view others accounts
+-[ ] Username gets cut off if too long
+-[ ] Validate username length
+-[ ] Test caret doesn't keep up
\ No newline at end of file
diff --git a/about/Coursework Specification.pdf b/about/Coursework Specification.pdf
new file mode 100644
index 0000000..47f21dd
Binary files /dev/null and b/about/Coursework Specification.pdf differ
diff --git a/about/TODO.md b/about/TODO.md
new file mode 100644
index 0000000..b08cddd
--- /dev/null
+++ b/about/TODO.md
@@ -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
\ No newline at end of file
diff --git a/database/dev/database.sqlite b/database/dev/database.sqlite
index 04cbb34..33b094f 100755
Binary files a/database/dev/database.sqlite and b/database/dev/database.sqlite differ
diff --git a/public/Admin/Delete User/index.html b/public/Admin/Delete User/index.html
new file mode 100755
index 0000000..4d8c698
--- /dev/null
+++ b/public/Admin/Delete User/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Admin Panel
+
+
+
+
+
+
+
Password
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/Admin/Delete User/index.js b/public/Admin/Delete User/index.js
new file mode 100755
index 0000000..b15db80
--- /dev/null
+++ b/public/Admin/Delete User/index.js
@@ -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);
+}
\ No newline at end of file
diff --git a/public/Admin/index.css b/public/Admin/index.css
new file mode 100755
index 0000000..cf59249
--- /dev/null
+++ b/public/Admin/index.css
@@ -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%;
+}
\ No newline at end of file
diff --git a/public/Admin/index.html b/public/Admin/index.html
new file mode 100755
index 0000000..6e4bb39
--- /dev/null
+++ b/public/Admin/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Admin Panel
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/api/api.js b/public/api/api.js
new file mode 100644
index 0000000..3368602
--- /dev/null
+++ b/public/api/api.js
@@ -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;
+ };
+ }
+}
\ No newline at end of file
diff --git a/public/api/user.js b/public/api/user.js
new file mode 100755
index 0000000..b29d00f
--- /dev/null
+++ b/public/api/user.js
@@ -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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/assets/favicon/android-chrome-192x192.png b/public/assets/favicon/android-chrome-192x192.png
new file mode 100755
index 0000000..5505cf6
Binary files /dev/null and b/public/assets/favicon/android-chrome-192x192.png differ
diff --git a/public/assets/favicon/android-chrome-512x512.png b/public/assets/favicon/android-chrome-512x512.png
new file mode 100755
index 0000000..8cc7222
Binary files /dev/null and b/public/assets/favicon/android-chrome-512x512.png differ
diff --git a/public/assets/favicon/apple-touch-icon.png b/public/assets/favicon/apple-touch-icon.png
new file mode 100755
index 0000000..69ceb13
Binary files /dev/null and b/public/assets/favicon/apple-touch-icon.png differ
diff --git a/public/assets/favicon/favicon-16x16.png b/public/assets/favicon/favicon-16x16.png
new file mode 100755
index 0000000..3600c33
Binary files /dev/null and b/public/assets/favicon/favicon-16x16.png differ
diff --git a/public/assets/favicon/favicon-32x32.png b/public/assets/favicon/favicon-32x32.png
new file mode 100755
index 0000000..f924768
Binary files /dev/null and b/public/assets/favicon/favicon-32x32.png differ
diff --git a/public/assets/favicon/favicon.ico b/public/assets/favicon/favicon.ico
new file mode 100755
index 0000000..95c11d6
Binary files /dev/null and b/public/assets/favicon/favicon.ico differ
diff --git a/public/assets/favicon/site.webmanifest b/public/assets/favicon/site.webmanifest
new file mode 100755
index 0000000..45dc8a2
--- /dev/null
+++ b/public/assets/favicon/site.webmanifest
@@ -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"}
\ No newline at end of file
diff --git a/public/assets/fonts/RobotoMono-Medium.ttf b/public/assets/fonts/RobotoMono-Medium.ttf
new file mode 100755
index 0000000..752d0fa
Binary files /dev/null and b/public/assets/fonts/RobotoMono-Medium.ttf differ
diff --git a/public/assets/icons/account_circle.svg b/public/assets/icons/account_circle.svg
new file mode 100644
index 0000000..ce59194
--- /dev/null
+++ b/public/assets/icons/account_circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/components/button.js b/public/components/button.js
new file mode 100755
index 0000000..2a1e752
--- /dev/null
+++ b/public/components/button.js
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/public/components/canvas.js b/public/components/canvas.js
new file mode 100755
index 0000000..7aa8eac
--- /dev/null
+++ b/public/components/canvas.js
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/public/components/menu.js b/public/components/menu.js
new file mode 100755
index 0000000..b9a74c9
--- /dev/null
+++ b/public/components/menu.js
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/public/components/textbox.js b/public/components/textbox.js
new file mode 100755
index 0000000..cebccd7
--- /dev/null
+++ b/public/components/textbox.js
@@ -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) {
+
+ }
+}
\ No newline at end of file
diff --git a/public/components/timemenu.js b/public/components/timemenu.js
new file mode 100755
index 0000000..047bee3
--- /dev/null
+++ b/public/components/timemenu.js
@@ -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 };
+ }
+}
\ No newline at end of file
diff --git a/public/components/timer.js b/public/components/timer.js
new file mode 100755
index 0000000..950be0a
--- /dev/null
+++ b/public/components/timer.js
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/components/user.js b/public/components/user.js
new file mode 100644
index 0000000..a816a4d
--- /dev/null
+++ b/public/components/user.js
@@ -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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/index.css b/public/index.css
new file mode 100755
index 0000000..b7e9549
--- /dev/null
+++ b/public/index.css
@@ -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;
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100755
index 0000000..5933f71
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ TypeFast
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/index.js b/public/index.js
new file mode 100644
index 0000000..5dc26ee
--- /dev/null
+++ b/public/index.js
@@ -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();
+}
\ No newline at end of file
diff --git a/public/screens/accountScreen.js b/public/screens/accountScreen.js
new file mode 100755
index 0000000..bf793dc
--- /dev/null
+++ b/public/screens/accountScreen.js
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/endscreen.js b/public/screens/endscreen.js
new file mode 100755
index 0000000..d99b244
--- /dev/null
+++ b/public/screens/endscreen.js
@@ -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());
+ }
+}
\ No newline at end of file
diff --git a/public/screens/leaderboardscreen.js b/public/screens/leaderboardscreen.js
new file mode 100755
index 0000000..ef8b57a
--- /dev/null
+++ b/public/screens/leaderboardscreen.js
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/loginscreen.js b/public/screens/loginscreen.js
new file mode 100755
index 0000000..20802f0
--- /dev/null
+++ b/public/screens/loginscreen.js
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/profilescreen.js b/public/screens/profilescreen.js
new file mode 100755
index 0000000..6837d02
--- /dev/null
+++ b/public/screens/profilescreen.js
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/screenmanager.js b/public/screens/screenmanager.js
new file mode 100755
index 0000000..824998e
--- /dev/null
+++ b/public/screens/screenmanager.js
@@ -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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/settingsScreen.js b/public/screens/settingsScreen.js
new file mode 100755
index 0000000..1c44857
--- /dev/null
+++ b/public/screens/settingsScreen.js
@@ -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");
+ }
+}
\ No newline at end of file
diff --git a/public/screens/signUpScreen.js b/public/screens/signUpScreen.js
new file mode 100755
index 0000000..95f2691
--- /dev/null
+++ b/public/screens/signUpScreen.js
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/startscreen.js b/public/screens/startscreen.js
new file mode 100755
index 0000000..6d5aa41
--- /dev/null
+++ b/public/screens/startscreen.js
@@ -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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/screens/testscreen.js b/public/screens/testscreen.js
new file mode 100755
index 0000000..a96e7e7
--- /dev/null
+++ b/public/screens/testscreen.js
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100644
index 0000000..9b94640
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,2 @@
+#/bin/sh
+docker build -t "typing-website:Dockerfile" .
\ No newline at end of file
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100755
index 0000000..f1995f8
--- /dev/null
+++ b/scripts/run.sh
@@ -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" \
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index c7b86d9..4f8b451 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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 {
rocket::build()
- .attach(CORS)
+ // .attach(CORS)
// testing only, should return "Hello world"
.mount("/test", routes![test])
// hosts the api routes necessary for the website
diff --git a/src/typing/sql.rs b/src/typing/sql.rs
index 42f9b73..68b0815 100644
--- a/src/typing/sql.rs
+++ b/src/typing/sql.rs
@@ -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>) -> 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(())
diff --git a/src/typing/test.rs b/src/typing/test.rs
index 7f339c7..73ba8b7 100644
--- a/src/typing/test.rs
+++ b/src/typing/test.rs
@@ -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 = "")]
pub async fn create_test(test: Json>, database: &State) {
- 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}"); }
}
}
diff --git a/websites/Typing/Admin/Delete User/index.html b/websites/Typing/Admin/Delete User/index.html
new file mode 100755
index 0000000..4d8c698
--- /dev/null
+++ b/websites/Typing/Admin/Delete User/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Admin Panel
+
+
+
+
+
+
+
Password
+
+
+
+
+
+
\ No newline at end of file
diff --git a/websites/Typing/Admin/Delete User/index.js b/websites/Typing/Admin/Delete User/index.js
new file mode 100755
index 0000000..b15db80
--- /dev/null
+++ b/websites/Typing/Admin/Delete User/index.js
@@ -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);
+}
\ No newline at end of file
diff --git a/websites/Typing/Admin/index.css b/websites/Typing/Admin/index.css
new file mode 100755
index 0000000..cf59249
--- /dev/null
+++ b/websites/Typing/Admin/index.css
@@ -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%;
+}
\ No newline at end of file
diff --git a/websites/Typing/Admin/index.html b/websites/Typing/Admin/index.html
new file mode 100755
index 0000000..6e4bb39
--- /dev/null
+++ b/websites/Typing/Admin/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Admin Panel
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/websites/Typing/api/api.js b/websites/Typing/api/api.js
index b9a2300..1b451cc 100644
--- a/websites/Typing/api/api.js
+++ b/websites/Typing/api/api.js
@@ -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();
diff --git a/websites/Typing/api/user.js b/websites/Typing/api/user.js
old mode 100644
new mode 100755
index 587fe3a..0fa49d7
--- a/websites/Typing/api/user.js
+++ b/websites/Typing/api/user.js
@@ -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"
}
diff --git a/websites/Typing/assets/favicon/android-chrome-192x192.png b/websites/Typing/assets/favicon/android-chrome-192x192.png
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/favicon/android-chrome-512x512.png b/websites/Typing/assets/favicon/android-chrome-512x512.png
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/favicon/apple-touch-icon.png b/websites/Typing/assets/favicon/apple-touch-icon.png
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/favicon/favicon-16x16.png b/websites/Typing/assets/favicon/favicon-16x16.png
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/favicon/favicon-32x32.png b/websites/Typing/assets/favicon/favicon-32x32.png
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/favicon/favicon.ico b/websites/Typing/assets/favicon/favicon.ico
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/favicon/site.webmanifest b/websites/Typing/assets/favicon/site.webmanifest
old mode 100644
new mode 100755
diff --git a/websites/Typing/assets/fonts/RobotoMono-Medium.ttf b/websites/Typing/assets/fonts/RobotoMono-Medium.ttf
old mode 100644
new mode 100755
diff --git a/websites/Typing/index.css b/websites/Typing/index.css
old mode 100644
new mode 100755
diff --git a/websites/Typing/index.html b/websites/Typing/index.html
old mode 100644
new mode 100755
diff --git a/websites/Typing/index.js b/websites/Typing/index.js
index 551e731..d9c7de0 100644
--- a/websites/Typing/index.js
+++ b/websites/Typing/index.js
@@ -32,7 +32,7 @@ function setup() {
user = new User();
screenManager.setScreen(new StartScreen());
- api.login();
+ api.login(null, null, true);
api.getTest();
textFont(roboto);
}
diff --git a/websites/Typing/lib/p5.js b/websites/Typing/lib/p5.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/lib/p5.min.js b/websites/Typing/lib/p5.min.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/accountScreen.js b/websites/Typing/screens/accountScreen.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/endscreen.js b/websites/Typing/screens/endscreen.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/leaderboardscreen.js b/websites/Typing/screens/leaderboardscreen.js
old mode 100644
new mode 100755
index bd36a1b..dd487d6
--- a/websites/Typing/screens/leaderboardscreen.js
+++ b/websites/Typing/screens/leaderboardscreen.js
@@ -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
diff --git a/websites/Typing/screens/loginscreen.js b/websites/Typing/screens/loginscreen.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/profilescreen.js b/websites/Typing/screens/profilescreen.js
old mode 100644
new mode 100755
index 6155cad..943ae11
--- a/websites/Typing/screens/profilescreen.js
+++ b/websites/Typing/screens/profilescreen.js
@@ -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;
}
diff --git a/websites/Typing/screens/screenmanager.js b/websites/Typing/screens/screenmanager.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/settingsScreen.js b/websites/Typing/screens/settingsScreen.js
old mode 100644
new mode 100755
index 679ce3d..88f80a8
--- a/websites/Typing/screens/settingsScreen.js
+++ b/websites/Typing/screens/settingsScreen.js
@@ -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);
diff --git a/websites/Typing/screens/signUpScreen.js b/websites/Typing/screens/signUpScreen.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/startscreen.js b/websites/Typing/screens/startscreen.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/screens/testscreen.js b/websites/Typing/screens/testscreen.js
old mode 100644
new mode 100755
index 8a55a93..a96e7e7
--- a/websites/Typing/screens/testscreen.js
+++ b/websites/Typing/screens/testscreen.js
@@ -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) {
diff --git a/websites/Typing/ui_elements/button.js b/websites/Typing/ui_elements/button.js
old mode 100644
new mode 100755
index 90e05cc..2a1e752
--- a/websites/Typing/ui_elements/button.js
+++ b/websites/Typing/ui_elements/button.js
@@ -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) {
diff --git a/websites/Typing/ui_elements/canvas.js b/websites/Typing/ui_elements/canvas.js
old mode 100644
new mode 100755
diff --git a/websites/Typing/ui_elements/menu.js b/websites/Typing/ui_elements/menu.js
old mode 100644
new mode 100755
index be3a508..16216e8
--- a/websites/Typing/ui_elements/menu.js
+++ b/websites/Typing/ui_elements/menu.js
@@ -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")
]
}
diff --git a/websites/Typing/ui_elements/textbox.js b/websites/Typing/ui_elements/textbox.js
old mode 100644
new mode 100755
index e74c7ee..3f6392b
--- a/websites/Typing/ui_elements/textbox.js
+++ b/websites/Typing/ui_elements/textbox.js
@@ -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) {
+
+ }
}
\ No newline at end of file
diff --git a/websites/Typing/ui_elements/timemenu.js b/websites/Typing/ui_elements/timemenu.js
old mode 100644
new mode 100755
index 8f40682..047bee3
--- a/websites/Typing/ui_elements/timemenu.js
+++ b/websites/Typing/ui_elements/timemenu.js
@@ -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 };
}
}
\ No newline at end of file
diff --git a/websites/Typing/ui_elements/timer.js b/websites/Typing/ui_elements/timer.js
old mode 100644
new mode 100755