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/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 ee629d1..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