17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM rust:1 AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN apt-get -y update && apt-get -y upgrade
|
||||
RUN apt-get install -y sqlite3 libsqlite3-dev
|
||||
|
||||
ENV DATABASE_URL="sqlite:/app/database/dev/database.sqlite"
|
||||
RUN cargo build --release
|
||||
|
||||
FROM ubuntu:latest as runner
|
||||
WORKDIR /app
|
||||
COPY ./wordlist.txt ./wordlist.txt
|
||||
COPY ./Rocket.toml ./Rocket.toml
|
||||
COPY --from=builder /app/target/release/cs_coursework /usr/local/bin/webapp
|
||||
ENV DATABASE_URL="sqlite:/app/database/dev/database.sqlite"
|
||||
CMD [ "webapp" ]
|
23
Rocket.toml
Normal file
@ -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
|
5
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
|
||||
-[ ] Username gets cut off if too long
|
||||
-[ ] Validate username length
|
||||
-[ ] Test caret doesn't keep up
|
BIN
about/Coursework Specification.pdf
Normal file
41
about/TODO.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Backend
|
||||
-[x] Migrate to sqlx as backend database queryer
|
||||
-[ ] Allow Private Accounts
|
||||
-[ ] Secrets to authenicate that its the user
|
||||
-[ ] Account Profile
|
||||
-[ ] Better Password Security
|
||||
-[ ] Login with google
|
||||
-[ ] Give new test route a more descriptive name
|
||||
-[x] Remove need for cors
|
||||
|
||||
# Frontend
|
||||
## API
|
||||
-[ ] Make js api asynchronous
|
||||
-[ ] Hash passwords on front end so server plaintext password is never sent over the internet
|
||||
-[x] Use secret to uniquely identify users
|
||||
|
||||
## Components
|
||||
-[x] Scrollbars
|
||||
-[ ] Scrollbars shouldn't appear if there's not enought content
|
||||
-[ ] Scrollbar update
|
||||
-[ ] Account Component
|
||||
-[ ] Username gets cut off if too long
|
||||
-[ ] Test caret doesn't display properley
|
||||
|
||||
## Screens
|
||||
-[x] Change Color Scheme
|
||||
-[ ] Color Scheme Editor
|
||||
-[ ] Be able to view others accounts
|
||||
|
||||
## Users
|
||||
-[ ] Validate username length
|
||||
-[ ] Validate usernames
|
||||
-[x] Login bug, text goes wrong color when removeing all text from a box
|
||||
|
||||
## API
|
||||
-[ ] Give useful errors to the user when something goes wrong
|
||||
|
||||
## Admin
|
||||
-[ ] Authentication
|
||||
-[ ] Allow deleting users
|
||||
-[ ] Allow deleting tests
|
26
public/Admin/Delete User/index.html
Executable file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel</title>
|
||||
|
||||
<link rel="stylesheet" href="../index.css">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<p>Arlo Filley</p>
|
||||
<ul>
|
||||
<li><a href="../">Home</a></li>
|
||||
<li><a href="./">Delete A User</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h1>Password</h1>
|
||||
<input id="password" type="password">
|
||||
<table>
|
||||
<tbody id="table"></tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
51
public/Admin/Delete User/index.js
Executable file
@ -0,0 +1,51 @@
|
||||
get();
|
||||
|
||||
function get() {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `https://arlofilley.com/api/typing/leaderboard`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.responseType = 'json';
|
||||
xhr.send();
|
||||
xhr.onload = () => {
|
||||
json = xhr.response
|
||||
createTable(json);
|
||||
};
|
||||
}
|
||||
|
||||
function createTable(pJson) {
|
||||
|
||||
let table = document.getElementById("table");
|
||||
console.log(password)
|
||||
|
||||
pJson.forEach(element => {
|
||||
let tr = document.createElement('tr');
|
||||
let button = document.createElement('button');
|
||||
let username = element.username;
|
||||
button.textContent = "delete"
|
||||
button.addEventListener( "click", () => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
let password = document.getElementById("password").value;
|
||||
xhr.open('GET', `https://arlofilley.com/api/typing/delete_user/${password}/${username}/120932187`);
|
||||
xhr.send();
|
||||
})
|
||||
|
||||
create_table_element(tr, "", [element.username, element.wpm], button)
|
||||
table.appendChild(tr)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function create_table_element(tr, string, elements, button) {
|
||||
if (elements.length > 0) {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let td = document.createElement('td');
|
||||
td.appendChild(document.createTextNode(elements[i]));
|
||||
tr.appendChild(td);
|
||||
}
|
||||
} else {
|
||||
let td = document.createElement('td');
|
||||
td.appendChild(document.createTextNode(string));
|
||||
tr.appendChild(td);
|
||||
}
|
||||
tr.appendChild(button);
|
||||
}
|
125
public/Admin/index.css
Executable file
@ -0,0 +1,125 @@
|
||||
:root {
|
||||
--navbar: #333333;
|
||||
|
||||
/* Navbar Button Properties */
|
||||
--navbar-button: #058ED9;
|
||||
--navbar-button-hover: #283F3B;
|
||||
--navbar-button-border: #058ED9;
|
||||
|
||||
/* */
|
||||
--background: #1f1f1f;
|
||||
|
||||
/* Table Properties*/
|
||||
--table: #FFD07B;
|
||||
--table-top:#FDB833;
|
||||
|
||||
/* Title Properties */
|
||||
--title-background: #333333;
|
||||
--title-text: #ffffff;
|
||||
|
||||
/* General Properties */
|
||||
--text: #000000;
|
||||
--border: #000000;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: #333;
|
||||
height: 75px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
nav p {
|
||||
margin-right: 700px;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 38px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
padding: 10px 20px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-left: 5%;
|
||||
margin-bottom: 0%;
|
||||
color: #fff;
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-left: 5%;
|
||||
margin-top: 1.5%;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 90%;
|
||||
border-collapse: collapse;
|
||||
margin: 0px;
|
||||
margin-left: 5%;
|
||||
margin-top: 30px;
|
||||
padding: 0px;
|
||||
border-spacing: 0px;
|
||||
border: 2px var(--border) solid;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
table tr:nth-child(n) {
|
||||
background: var(--table);
|
||||
}
|
||||
|
||||
table tr:nth-child(1) {
|
||||
background: var(--table-top);
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 1px black dotted;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 40%;
|
||||
padding: 8px;
|
||||
padding-left: 12.5%;
|
||||
padding-right: 12.5%;
|
||||
}
|
20
public/Admin/index.html
Executable file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel</title>
|
||||
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<p>Arlo Filley</p>
|
||||
<ul>
|
||||
<li><a href="./">Home</a></li>
|
||||
<li><a href="./Delete User/">Delete A User</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
292
public/api/api.js
Normal file
@ -0,0 +1,292 @@
|
||||
/**
|
||||
* @file This file provides abstracted functions to interact with the api
|
||||
* @author Arlo Filley
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class provides all the useful methods to interact with the api.
|
||||
*/
|
||||
class API {
|
||||
|
||||
constructor() { this.url = "/api"; }
|
||||
|
||||
/**
|
||||
* This takes the validated data and makes a post
|
||||
* request to the rocket server
|
||||
* @param {String} testType
|
||||
* @param {int} testLength
|
||||
* @param {int} testTime
|
||||
* @param {int} testSeed
|
||||
* @param {int} quoteId
|
||||
* @param {int} wpm
|
||||
* @param {int} accuracy
|
||||
* @param {int} userId
|
||||
*/
|
||||
postTest(pTestType, pTestLength, pTestTime, pTestSeed, pQuoteId, pWpm, pAccuracy, pUserId) {
|
||||
const data = {
|
||||
'test_type': pTestType,
|
||||
'test_length': pTestLength,
|
||||
'test_time': pTestTime,
|
||||
'test_seed': pTestSeed,
|
||||
'quote_id': pQuoteId,
|
||||
'wpm': pWpm,
|
||||
'accuracy': pAccuracy,
|
||||
'user_id': pUserId,
|
||||
'secret': user.secret
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(
|
||||
"POST",
|
||||
`${this.url}/post_test/`
|
||||
);
|
||||
|
||||
xhr.send(
|
||||
JSON.stringify(data)
|
||||
);
|
||||
|
||||
user.lastTest = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates all the parameters used for the postTest function which it then calls
|
||||
*/
|
||||
validateTest() {
|
||||
const test = screenManager.screen.textbox.getWords();
|
||||
const testType = "words";
|
||||
let testLength = test.length;
|
||||
let testTime = screenManager.screen.timer.getTime();
|
||||
const testSeed = 0;
|
||||
const quoteId = 0;
|
||||
let wpm;
|
||||
const userId = Number(user.userId);
|
||||
let test_content = screenManager.screen.textbox.getTestContent();
|
||||
|
||||
let string = "";
|
||||
let inaccurateLetters = 0;
|
||||
for (let letter = 0; letter < test.length; letter++) {
|
||||
if (test[letter] === test_content[letter]) {
|
||||
string += test[letter];
|
||||
} else {
|
||||
inaccurateLetters += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const accuracy = Math.round(((test.length - inaccurateLetters) / test.length) * 100);
|
||||
|
||||
// this is the wpm calculation factoring in the time of test
|
||||
// it assumes that all words are 5 characters long because on average
|
||||
// they are
|
||||
wpm = Math.round((string.length / 5) * (60 / testTime));
|
||||
|
||||
// the following code is a series of if statements that checks the
|
||||
// types of the variables is correct if not it errors it and returns
|
||||
// out of the function
|
||||
|
||||
if ( typeof testType !== "string" ) {
|
||||
console.error(`testType is value ${typeof testType}\nshould be a string`);
|
||||
return;
|
||||
}
|
||||
if ( typeof testLength !== "number") {
|
||||
console.error(`testLength is value ${typeof testLength}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
if ( typeof testTime !== "number") {
|
||||
console.error(`testTime is value ${typeof testTime}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
if ( typeof testSeed !== "number") {
|
||||
console.error(`testSeed is value ${typeof testSeed}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
if ( typeof quoteId !== "number") {
|
||||
console.error(`quoteId is value ${typeof quoteId}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
if ( typeof wpm !== "number") {
|
||||
console.error(`wpm is value ${typeof wpm}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
if ( typeof accuracy !== "number") {
|
||||
console.error(`accuracy is value ${typeof accuracy}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
if ( typeof userId !== "number") {
|
||||
console.error(`userId is value ${typeof userId}\n should be a number`);
|
||||
return;
|
||||
}
|
||||
|
||||
// after checking that all variables are of the correct type these if statements check
|
||||
// that they are acceptable values or are in acceptable bounds depending on variable types
|
||||
|
||||
if (testType !== "words") {
|
||||
// currently words is the only acceptable type but
|
||||
// this will change in later iterations
|
||||
|
||||
console.error(`testType is invalid\nacceptable options ['words']`);
|
||||
}
|
||||
// upper bounds for these numbers are less of a concern because the server will automatically
|
||||
// return an error if values are over the limit
|
||||
if (testLength < 0) {
|
||||
console.error(`testLength is too small, min value 0`)
|
||||
}
|
||||
if (testTime < 1) {
|
||||
console.error(`testTime is too small, min value 1`)
|
||||
}
|
||||
if (testSeed < 0) {
|
||||
console.error(`testSeed is too small, min value 0`)
|
||||
}
|
||||
if (quoteId < 0) {
|
||||
console.error(`quoteId is too small, min value 0`)
|
||||
}
|
||||
if (wpm < 0) {
|
||||
console.error(`wpm is too small, min value 0`)
|
||||
}
|
||||
// accuracy needs an upper bound check because users can't have more than 100%
|
||||
// accuracy when completing their tests
|
||||
if (accuracy < 0) {
|
||||
console.error(`accuracy is too small, min value 0`)
|
||||
} else if (accuracy > 100) {
|
||||
console.error(`accuracy is too big, max value 100`)
|
||||
}
|
||||
if (userId < 0) {
|
||||
console.error(`userId is too small, min value 0`)
|
||||
}
|
||||
|
||||
// there will be other tests here in later iterations but for now these tests should suffice
|
||||
|
||||
this.postTest(testType, testLength, testTime, testSeed, quoteId, wpm, accuracy, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* takes a validated name and password and sends
|
||||
* a post request to make a user with the given
|
||||
* username and password
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns
|
||||
*/
|
||||
createUser( username, password ) {
|
||||
console.log( username, password );
|
||||
const user = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open( "POST", `${this.url}/create_user/` );
|
||||
|
||||
xhr.send( JSON.stringify(user) );
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 500) {
|
||||
alert("Sorry, looks like your username isn't unique");
|
||||
console.error("Sorry, looks like your username isn't unique")
|
||||
} else {
|
||||
this.login(username, password);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* takes a validated name and password and sends
|
||||
* a post request to make a user with the given
|
||||
* username and password
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @param {boolean} initial
|
||||
* @returns
|
||||
*/
|
||||
login(pUsername, pPassword, initial = false) {
|
||||
// If Local Storage has the information we need there is no need to make a request to the server
|
||||
if (localStorage.getItem("username") === pUsername || (initial && localStorage.length === 3) ) {
|
||||
user.userId = localStorage.getItem("userId");
|
||||
user.secret = localStorage.getItem("secret");
|
||||
user.username = localStorage.getItem("username");
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Variable Validation
|
||||
if (pUsername == undefined || pPassword == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `${this.url}/login/${pUsername}/${pPassword}`);
|
||||
xhr.send();
|
||||
xhr.onload = () => {
|
||||
let response = JSON.parse(xhr.response);
|
||||
|
||||
// If there is an error with the login we need
|
||||
if (xhr.response === null) {
|
||||
alert("Error Logging in, maybe check your password");
|
||||
return
|
||||
}
|
||||
|
||||
user.userId = response.user_id;
|
||||
user.username = pUsername
|
||||
user.secret = response.secret;
|
||||
|
||||
localStorage.setItem("userId", user.userId);
|
||||
localStorage.setItem("username", pUsername);
|
||||
localStorage.setItem("secret", user.secret);
|
||||
};
|
||||
}
|
||||
|
||||
logout() {
|
||||
user = new User();
|
||||
user.username = "no one";
|
||||
user.password = "";
|
||||
user.userId = 0;
|
||||
user.tests = [];
|
||||
localStorage.clear();
|
||||
this.getTest();
|
||||
}
|
||||
|
||||
getUserTests() {
|
||||
if (user.userId === 0) {
|
||||
user.tests = undefined;
|
||||
return;
|
||||
}
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', `${this.url}/get_tests/${user.userId}/${user.secret}`);
|
||||
xhr.send();
|
||||
xhr.onload = () => {
|
||||
user.tests = JSON.parse(xhr.response);
|
||||
};
|
||||
}
|
||||
|
||||
getLeaderBoard() {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `${this.url}/leaderboard/`);
|
||||
xhr.send();
|
||||
xhr.onload = () => {
|
||||
user.leaderboard = JSON.parse(xhr.response);
|
||||
};
|
||||
}
|
||||
|
||||
getTest() {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `${this.url}/new_test/`);
|
||||
xhr.send();
|
||||
xhr.onload = () =>{
|
||||
const effectiveWidth = (windowWidth - 200) / 13;
|
||||
let textArr = JSON.parse(xhr.response);
|
||||
let finalText = [];
|
||||
let text = "";
|
||||
for (let i = 0; i < textArr.length; i++) {
|
||||
if (text.length + textArr[i].length < effectiveWidth) {
|
||||
text += `${textArr[i]} `
|
||||
} else {
|
||||
finalText.push(text.substring(0,text.length-1));
|
||||
text = `${textArr[i]} `;
|
||||
}
|
||||
}
|
||||
user.nextTest = finalText;
|
||||
};
|
||||
}
|
||||
}
|
42
public/api/user.js
Executable file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
BIN
public/assets/favicon/android-chrome-192x192.png
Executable file
After Width: | Height: | Size: 3.4 KiB |
BIN
public/assets/favicon/android-chrome-512x512.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/favicon/apple-touch-icon.png
Executable file
After Width: | Height: | Size: 3.0 KiB |
BIN
public/assets/favicon/favicon-16x16.png
Executable file
After Width: | Height: | Size: 552 B |
BIN
public/assets/favicon/favicon-32x32.png
Executable file
After Width: | Height: | Size: 968 B |
BIN
public/assets/favicon/favicon.ico
Executable file
After Width: | Height: | Size: 15 KiB |
1
public/assets/favicon/site.webmanifest
Executable file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
public/assets/fonts/RobotoMono-Medium.ttf
Executable file
1
public/assets/icons/account_circle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"/></svg>
|
After Width: | Height: | Size: 732 B |
215
public/components/button.js
Executable file
@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @file This file provides the button class, which can
|
||||
* be checked for clicks
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - implement visual changes (borders, etc)
|
||||
* - replace with methods with getters and setters
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Button class, a rectangle that can be checked for mouse clicks
|
||||
*/
|
||||
class Button {
|
||||
// this is the doc comment for the Timer class
|
||||
/**
|
||||
* @param {int} pX
|
||||
* @param {int} pY
|
||||
* @param {int} pWidth
|
||||
* @param {int} pHeight
|
||||
* @param {int} pLayer
|
||||
* @param {bool} pVisible
|
||||
* @param {hexcode} pTextColor
|
||||
* @param {bool} pBorder
|
||||
* @param {hexcode} pBorderColor
|
||||
* @param {hexcode} pBackgroundColor
|
||||
* @param {int} pTime
|
||||
* @param {bool} pBar
|
||||
* @param {string} Label
|
||||
*/
|
||||
constructor(pX = 100, pY = 100,
|
||||
pWidth = 200, pHeight = 30,
|
||||
pLabel = "Default Button",
|
||||
|
||||
pBorder = false,
|
||||
pHoverBorder = true,
|
||||
|
||||
pTextColor = user.colorScheme.buttonText,
|
||||
pBorderColor = user.colorScheme.buttonBorder,
|
||||
pBackgroundColor = user.colorScheme.buttonBG,
|
||||
|
||||
pHoverTextColor = user.colorScheme.buttonHoverText,
|
||||
pHoverBorderColor = user.colorScheme.buttonHoverBorder,
|
||||
pHoverBackgroundColor = user.colorScheme.buttonHoverBG,
|
||||
|
||||
pLayer = 0, pVisible = true,
|
||||
) {
|
||||
this.x = pX;
|
||||
this.y = pY;
|
||||
this.width = pWidth;
|
||||
this.height = pHeight;
|
||||
this.layer = pLayer;
|
||||
this.visible = pVisible;
|
||||
this.textColor = pTextColor;
|
||||
this.border = pBorder;
|
||||
this.borderColor = pBorderColor;
|
||||
this.backgroundColor = pBackgroundColor;
|
||||
this.label = pLabel;
|
||||
|
||||
// Attributes to control the look of the button
|
||||
// when the user is hovering over it
|
||||
this.hoverBorder = pHoverBorder;
|
||||
this.hoverBorderColor = pHoverBorderColor;
|
||||
this.hoverTextColor = pHoverTextColor;
|
||||
this.hoverBackgroundColor = pHoverBackgroundColor;
|
||||
}
|
||||
|
||||
getx() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
setX(pX) {
|
||||
this.x = pX;
|
||||
}
|
||||
|
||||
getY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
setY(pY) {
|
||||
this.y = pY;
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
setWidth(pWidth) {
|
||||
this.width = pWidth;
|
||||
}
|
||||
|
||||
getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
setHeight(pHeight) {
|
||||
this.height = pHeight;
|
||||
}
|
||||
|
||||
getLayer() {
|
||||
return this.layer;
|
||||
}
|
||||
|
||||
setLayer(pLayer) {
|
||||
this.layer = pLayer;
|
||||
}
|
||||
|
||||
getVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
setVisible(pVisible) {
|
||||
this.visible = pVisible;
|
||||
}
|
||||
|
||||
getTextColor() {
|
||||
return this.textColor;
|
||||
}
|
||||
|
||||
setTextColor(pTextColor) {
|
||||
this.textColor = pTextColor;
|
||||
}
|
||||
|
||||
getBorder() {
|
||||
return this.border;
|
||||
}
|
||||
|
||||
setBorder(pBorder) {
|
||||
this.border = pBorder;
|
||||
}
|
||||
|
||||
getBorderColor() {
|
||||
return this.borderColor;
|
||||
}
|
||||
|
||||
setBorderColor(pBorderColor) {
|
||||
this.borderColor = pBorderColor;
|
||||
}
|
||||
|
||||
getBackgroundColor() {
|
||||
return this.backgroundColor;
|
||||
}
|
||||
|
||||
setBackgroundColor(pBackgroundColor) {
|
||||
this.backgroundColor = pBackgroundColor;
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
setLabel(pLabel) {
|
||||
this.label = pLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions returns more
|
||||
*/
|
||||
isPressed() {
|
||||
if (!this.visible) {
|
||||
return;
|
||||
} else if (!mouseIsPressed) { // a unique p5.js value that checks if the mouse is clicked
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the mouse is within the bounds of the return that the button has been pressed
|
||||
if (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function draws the button with the label
|
||||
*/
|
||||
draw() {
|
||||
if (!this.visible) {
|
||||
return;
|
||||
}
|
||||
textSize(20);
|
||||
textAlign(CENTER, CENTER);
|
||||
|
||||
if (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height) {
|
||||
|
||||
if (this.hoverBorder) {
|
||||
strokeWeight(2);
|
||||
stroke(this.hoverBorderColor)
|
||||
} else {
|
||||
noStroke();
|
||||
}
|
||||
fill(this.hoverBackgroundColor);
|
||||
rect(this.x, this.y, this.width, this.height);
|
||||
|
||||
noStroke();
|
||||
fill(this.hoverTextColor);
|
||||
text(this.label, this.x, this.y, this.width, this.height);
|
||||
} else {
|
||||
if (this.border) {
|
||||
strokeWeight(2);
|
||||
stroke(this.borderColor)
|
||||
} else {
|
||||
noStroke();
|
||||
}
|
||||
|
||||
fill(this.backgroundColor);
|
||||
rect(this.x, this.y, this.width, this.height);
|
||||
|
||||
noStroke();
|
||||
fill(this.textColor);
|
||||
text(this.label, this.x, this.y, this.width, this.height);
|
||||
}
|
||||
noStroke();
|
||||
}
|
||||
}
|
31
public/components/canvas.js
Executable file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file This file provides a canvas class wrapper for the p5.js canvas
|
||||
* @author Arlo Filley
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class provides a wrapper around the
|
||||
* p5.js canvas, with easier methods to work with.
|
||||
*/
|
||||
class Canvas {
|
||||
constructor() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.canvas = createCanvas(0, 0);
|
||||
}
|
||||
|
||||
|
||||
center() {
|
||||
this.canvas.position(this.x, this.y);
|
||||
}
|
||||
|
||||
|
||||
resize() {
|
||||
this.canvas.resize(windowWidth, windowHeight);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.canvas.resize(0, 0);
|
||||
}
|
||||
}
|
44
public/components/menu.js
Executable file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file This file provides a menu class to allow the user to easily navigate the site
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - more sensible button names for easier navigation
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class provides a menu with all the relevent buttons the user will need,
|
||||
* it also handles when the user presses a button, by creating the correct screen
|
||||
*/
|
||||
class Menu {
|
||||
constructor() {
|
||||
this.numButtons = 5;
|
||||
this.user = new UserShower((windowWidth / this.numButtons) * 0, 0, windowWidth / this.numButtons, 50);
|
||||
this.buttons = [
|
||||
// new Button((windowWidth / this.numButtons) * 0, 0, windowWidth / 5, 50, "Account"),
|
||||
new Button((windowWidth / this.numButtons) * 1, 0, windowWidth / this.numButtons, 50, "Test Data"),
|
||||
new Button((windowWidth / this.numButtons) * 2, 0, windowWidth / this.numButtons, 50, "Start Test"),
|
||||
new Button((windowWidth / this.numButtons) * 3, 0, windowWidth / this.numButtons, 50, "Leaderboard"),
|
||||
new Button((windowWidth / this.numButtons) * 4, 0, windowWidth / this.numButtons, 50, "Test Settings")
|
||||
]
|
||||
}
|
||||
|
||||
draw() {
|
||||
textAlign(CENTER, CENTER);
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw()
|
||||
}
|
||||
|
||||
if (this.buttons[0].isPressed()) {
|
||||
screenManager.setScreen(new ProfileScreen());
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
screenManager.setScreen(new TestScreen())
|
||||
} else if (this.buttons[2].isPressed()) {
|
||||
screenManager.setScreen(new LeaderboardScreen())
|
||||
} else if (this.buttons[3].isPressed()) {
|
||||
screenManager.setScreen(new SettingsScreen())
|
||||
}
|
||||
|
||||
this.user.draw()
|
||||
}
|
||||
}
|
364
public/components/textbox.js
Executable file
@ -0,0 +1,364 @@
|
||||
/**
|
||||
* @file This file provides the textbox class for taking user input
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - add all characters a user could press
|
||||
* - refactor the code displaying the characters. It can become slow after lots of typing
|
||||
* - password mode, where the charcters are hidden from the user
|
||||
* - getters and setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class takes input from the user and displays it some form
|
||||
* it handles the test input from the user, and the login and sign
|
||||
* up pages.
|
||||
*/
|
||||
class Textbox {
|
||||
/**
|
||||
* Creates a new instance of the Textbox class
|
||||
* @param {int} pX
|
||||
* @param {int} pY
|
||||
* @param {int} pWidth
|
||||
* @param {int} pHeight
|
||||
* @param {int} pLayer
|
||||
* @param {bool} pVisible
|
||||
* @param {hexcode} pTextColor
|
||||
* @param {bool} pBorder
|
||||
* @param {hexcode} pBorderColor
|
||||
* @param {hexcode} pBackgroundColor
|
||||
* @param {bool} pLine
|
||||
* @param {bool} pIsTest
|
||||
* @param {bool} pIsPassword
|
||||
*/
|
||||
constructor(
|
||||
pX, pY,
|
||||
pWidth, pHeight,
|
||||
pLayer, pVisible,
|
||||
pTextColor = user.colorScheme.text,
|
||||
pBorder, pBorderColor,
|
||||
pBackgroundColor,
|
||||
pLine, pIsTest,
|
||||
pIsPassword = false
|
||||
) {
|
||||
this.x = pX;
|
||||
this.y = pY;
|
||||
this.width = pWidth;
|
||||
this.height = pHeight;
|
||||
this.layer = pLayer;
|
||||
this.visible = pVisible;
|
||||
this.textColor = pTextColor;
|
||||
this.border = pBorder;
|
||||
this.borderColor = pBorderColor;
|
||||
this.backgroundColor = pBackgroundColor;
|
||||
|
||||
this.letters = [];
|
||||
this.allowedLetters = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
|
||||
'l', 'm','n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
|
||||
'x', 'y', 'z',
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
|
||||
'\'', '"', ',', '.', ' ', '!', '@', '$', '%', '^', '&', '*', '(', ')',
|
||||
]
|
||||
|
||||
this.line = pLine;
|
||||
this.isTest = pIsTest;
|
||||
|
||||
if (this.isTest) {
|
||||
this.testContent = user.nextTest;
|
||||
this.currentLine = 0;
|
||||
this.words = [""];
|
||||
} else {
|
||||
this.words = "";
|
||||
}
|
||||
|
||||
this.isPassword = pIsPassword;
|
||||
|
||||
this.goodColor = user.colorScheme.testGood;
|
||||
this.badColor = user.colorScheme.testBad;
|
||||
this.textColor = user.colorScheme.text;
|
||||
}
|
||||
|
||||
getX() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
setX(pX) {
|
||||
this.x = pX;
|
||||
}
|
||||
|
||||
getY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
setY(pY) {
|
||||
this.y = pY;
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
setWidth(pWidth) {
|
||||
this.width = pWidth;
|
||||
}
|
||||
|
||||
getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
setHeight(pHeight) {
|
||||
this.height = pHeight;
|
||||
}
|
||||
|
||||
getLayer() {
|
||||
return this.layer;
|
||||
}
|
||||
|
||||
setLayer(pLayer) {
|
||||
this.layer = pLayer;
|
||||
}
|
||||
|
||||
getVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
setVisible(pVisible) {
|
||||
this.visible = pVisible;
|
||||
}
|
||||
|
||||
getTextColor() {
|
||||
return this.textColor;
|
||||
}
|
||||
|
||||
setTextColor(pTextColor) {
|
||||
this.textColor = pTextColor;
|
||||
}
|
||||
|
||||
getBorder() {
|
||||
return this.border;
|
||||
}
|
||||
|
||||
setBorder(pBorder) {
|
||||
this.border = pBorder;
|
||||
}
|
||||
|
||||
getBorderColor() {
|
||||
return this.borderColor;
|
||||
}
|
||||
|
||||
setBorderColor(pBorderColor) {
|
||||
this.borderColor = pBorderColor;
|
||||
}
|
||||
|
||||
getBackgroundColor() {
|
||||
return this.backgroundColor;
|
||||
}
|
||||
|
||||
setBackgroundColor(pBackgroundColor) {
|
||||
this.backgroundColor = pBackgroundColor;
|
||||
}
|
||||
|
||||
getLetters() {
|
||||
return this.letters;
|
||||
}
|
||||
|
||||
setLetters(pLetters) {
|
||||
this.letters = pLetters;
|
||||
}
|
||||
|
||||
/**
|
||||
* takes a key and handles it in the textbox
|
||||
* @param {String} pKey
|
||||
* @returns
|
||||
*/
|
||||
letterTyped(pKey) {
|
||||
if (pKey === "Backspace" && this.letters.length > 0) {
|
||||
this.letters.pop();
|
||||
|
||||
if (this.isTest) {
|
||||
this.words[this.currentLine] = this.words[this.currentLine].substring(0, this.words[this.currentLine].length-1);
|
||||
} else {
|
||||
this.words = this.words.substring(0, this.words.length-1)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.allowedLetters.length; i++) {
|
||||
if (pKey.toLowerCase() === this.allowedLetters[i]) {
|
||||
this.letters.push(pKey);
|
||||
if (this.isTest) {
|
||||
this.words[this.currentLine] += pKey;
|
||||
} else {
|
||||
this.words += pKey;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getWords() {
|
||||
let text = "";
|
||||
for (let i = 0; i < this.words.length; i++) {
|
||||
text += this.words[i];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
setWords(pWords) {
|
||||
this.words = pWords;
|
||||
}
|
||||
|
||||
getAllowedLetters() {
|
||||
return this.allowedLetters;
|
||||
}
|
||||
|
||||
setAllowedLetters(pAllowedLetters) {
|
||||
this.allowedLetters = pAllowedLetters;
|
||||
}
|
||||
|
||||
getTestContent() {
|
||||
let text = "";
|
||||
for (let i = 0; i < this.testContent.length; i++) {
|
||||
text += this.testContent[i];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* draws a Textbox
|
||||
* @returns
|
||||
*/
|
||||
draw() {
|
||||
// doesn't render the textbox if it should not be visible to the user.
|
||||
if (!this.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
noStroke();
|
||||
// sets a border if there should be one
|
||||
if (this.border) {
|
||||
stroke(this.borderColor);
|
||||
strokeWeight(1);
|
||||
}
|
||||
|
||||
// sets the parameters of what the text should look like;
|
||||
fill(this.textColor);
|
||||
textSize(23);
|
||||
textAlign(LEFT);
|
||||
if (this.words.length === 0 && this.line) {
|
||||
fill("#000")
|
||||
rect(this.x, this.y-15, 1, 30)
|
||||
}
|
||||
|
||||
// these variables allow me to use the values of x and y while updating them
|
||||
let i = this.x;
|
||||
let j = this.y;
|
||||
|
||||
if (this.isTest) {
|
||||
let i = this.x;
|
||||
let j = this.y;
|
||||
|
||||
if (this.words[this.currentLine].length >= this.testContent[this.currentLine].length) {
|
||||
this.currentLine++;
|
||||
this.words.push("");
|
||||
}
|
||||
|
||||
if (this.currentLine > 0) {
|
||||
for (let x = 0; x < this.testContent[this.currentLine-1].length; x++) {
|
||||
if (x < this.words[this.currentLine-1].length) {
|
||||
if (this.words[this.currentLine-1][x] === this.testContent[this.currentLine-1][x]) {
|
||||
fill("#00AA0044");
|
||||
} else {
|
||||
fill("#AA000044");
|
||||
}
|
||||
} else {
|
||||
fill("#00044");
|
||||
}
|
||||
|
||||
text(this.testContent[this.currentLine-1][x], i, j);
|
||||
|
||||
i += 13;
|
||||
}
|
||||
j+= 30;
|
||||
}
|
||||
|
||||
i = this.x;
|
||||
|
||||
for (let x = 0; x < this.testContent[this.currentLine].length; x++) {
|
||||
if (x < this.words[this.currentLine].length) {
|
||||
if (this.words[this.currentLine][x] === this.testContent[this.currentLine][x]) {
|
||||
fill(this.goodColor);
|
||||
} else {
|
||||
fill(this.badColor);
|
||||
}
|
||||
} else {
|
||||
fill(this.textColor);
|
||||
}
|
||||
|
||||
text(this.testContent[this.currentLine][x], i, j);
|
||||
|
||||
if (this.letters.length > 0 && x == this.letters.length && this.line) {
|
||||
fill(this.textColor)
|
||||
rect(i, j-15, 1, 30)
|
||||
}
|
||||
i += 13;
|
||||
}
|
||||
|
||||
|
||||
|
||||
i = this.x;
|
||||
j += 30;
|
||||
|
||||
fill(this.textColor);
|
||||
for (let x = this.currentLine + 1; x < this.testContent.length; x++) {
|
||||
text(this.testContent[x], i, j);
|
||||
j += 30;
|
||||
}
|
||||
|
||||
} else if (this.isPassword) {
|
||||
fill(this.textColor);
|
||||
|
||||
// these variables allow me to use the values of x and y while updating them
|
||||
let i = this.x;
|
||||
let j = this.y;
|
||||
|
||||
for (let x = 0; x < this.letters.length; x++) {
|
||||
if (i > this.x + this.width) i = this.x, j += 30;
|
||||
if (this.letters[x] === "Enter") {
|
||||
i = this.x, j+= 30;
|
||||
} else {
|
||||
let char = "-";
|
||||
text(char, i, j);
|
||||
i += 13
|
||||
}
|
||||
if (this.letters.length > 0 && x == this.letters.length-1 && this.line) {
|
||||
rect(i, j-15, 1, 30)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// these variables allow me to use the values of x and y while updating them
|
||||
let i = this.x;
|
||||
let j = this.y;
|
||||
|
||||
// currently this loop just prints out every letter in the array, including any enter characters
|
||||
for (let x = 0; x < this.letters.length; x++) {
|
||||
if (i > this.x + this.width) i = this.x, j += 30;
|
||||
if (this.letters[x] === "Enter") {
|
||||
i = this.x, j+= 30;
|
||||
} else {
|
||||
text(this.letters[x], i, j);
|
||||
i += 13
|
||||
}
|
||||
if (this.letters.length > 0 && x == this.letters.length-1 && this.line) {
|
||||
fill(this.textColor)
|
||||
rect(i, j-15, 1, 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_line(line, y) {
|
||||
|
||||
}
|
||||
}
|
61
public/components/timemenu.js
Executable file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @file This file provides a time menu class for editing the length of a test
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - implement visual changes (borders, etc)
|
||||
* - replace with methods with getters and setters
|
||||
* - highlight which option the user has chosen in some way
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class displays a dropdown menu for the user where
|
||||
* they can edit the duration of a test
|
||||
*/
|
||||
class TimeMenu {
|
||||
constructor() {
|
||||
this.buttons = [
|
||||
new Button(900, 250, 100, 30, "15s"),
|
||||
new Button(900, 280, 100, 30, "30s"),
|
||||
new Button(900, 310, 100, 30, "45s"),
|
||||
new Button(900, 340, 100, 30, "60s"),
|
||||
];
|
||||
|
||||
this.topButton = this.buttons[0];
|
||||
this.dropDownButton = new Button(1000, 250, 30, 30, "v")
|
||||
this.dropdown = false;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.dropdown) {
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw()
|
||||
}
|
||||
|
||||
if (this.buttons[0].isPressed() && user.time != 15) {
|
||||
user.time = 15;
|
||||
this.topButton = new Button(900, 250, 100, 30, "15s");
|
||||
this.dropdown = false;
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
user.time = 30;
|
||||
this.topButton = new Button(900, 250, 100, 30, "30s");
|
||||
this.dropdown = false;
|
||||
} else if (this.buttons[2].isPressed()) {
|
||||
user.time = 45;
|
||||
this.topButton = new Button(900, 250, 100, 30, "45s");
|
||||
this.dropdown = false;
|
||||
} else if (this.buttons[3].isPressed()) {
|
||||
user.time = 60;
|
||||
this.topButton = new Button(900, 250, 100, 30, "60s");
|
||||
this.dropdown = false;
|
||||
}
|
||||
} else {
|
||||
this.topButton.draw();
|
||||
}
|
||||
|
||||
this.dropDownButton.draw();
|
||||
if (this.dropDownButton.isPressed()) {
|
||||
this.dropdown = true;
|
||||
} else if (mouseIsPressed) { this.dropdown = false };
|
||||
}
|
||||
}
|
222
public/components/timer.js
Executable file
@ -0,0 +1,222 @@
|
||||
/**
|
||||
* @file This file provides a time menu class for editing the length of a test
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - implement visual changes (borders, etc)
|
||||
* - fix the timer number becoming invisible after a
|
||||
* it drops below a certain amount of time
|
||||
* - use getters and setters
|
||||
* - use the millis() p5.js function for if the framerate becomes
|
||||
* slowed down by the amount being drawn to the screen
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class provides the timer, which handles when a test starts and ends as well
|
||||
* as providing a visual element for the user to see
|
||||
*/
|
||||
class Timer {
|
||||
/**
|
||||
* @param {int} pX
|
||||
* @param {int} pY
|
||||
* @param {int} pWidth
|
||||
* @param {int} pHeight
|
||||
* @param {int} pLayer
|
||||
* @param {bool} pVisible
|
||||
* @param {hexcode} pTextColor
|
||||
* @param {bool} pBorder
|
||||
* @param {hexcode} pBorderColor
|
||||
* @param {hexcode} pBackgroundColor
|
||||
* @param {int} pTime
|
||||
* @param {bool} pBar
|
||||
*/
|
||||
constructor(pX, pY, pWidth, pHeight, pLayer, pVisible, pTextColor, pBorder, pBorderColor, pBackgroundColor, pTime, pBar) {
|
||||
this.x = pX;
|
||||
this.y = pY;
|
||||
this.width = pWidth;
|
||||
this.height = pHeight;
|
||||
this.layer = pLayer;
|
||||
this.visible = pVisible;
|
||||
this.textColor = pTextColor;
|
||||
this.border = pBorder;
|
||||
this.borderColor = pBorderColor;
|
||||
this.backgroundColor = pBackgroundColor;
|
||||
|
||||
this.bar = pBar;
|
||||
this.startTime;
|
||||
this.time = pTime;
|
||||
this.timeElapsed = 0;
|
||||
this.ended;
|
||||
this.hasStarted = false;
|
||||
}
|
||||
|
||||
getX() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
setX(pX) {
|
||||
this.x = pX;
|
||||
}
|
||||
|
||||
getY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
setY(pY) {
|
||||
this.y = pY;
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
setWidth(pWidth) {
|
||||
this.width = pWidth;
|
||||
}
|
||||
|
||||
getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
setHeight(pHeight) {
|
||||
this.height = pHeight;
|
||||
}
|
||||
|
||||
getLayer() {
|
||||
return this.layer;
|
||||
}
|
||||
|
||||
setLayer(pLayer) {
|
||||
this.layer = pLayer;
|
||||
}
|
||||
|
||||
getVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
setVisible(pVisible) {
|
||||
this.visible = pVisible;
|
||||
}
|
||||
|
||||
getTextColor() {
|
||||
return this.textColor;
|
||||
}
|
||||
|
||||
setTextColor(pTextColor) {
|
||||
this.textColor = pTextColor;
|
||||
}
|
||||
|
||||
getBorder() {
|
||||
return this.border;
|
||||
}
|
||||
|
||||
setBorder(pBorder) {
|
||||
this.border = pBorder;
|
||||
}
|
||||
|
||||
getBorderColor() {
|
||||
return this.borderColor;
|
||||
}
|
||||
|
||||
setBorderColor(pBorderColor) {
|
||||
this.borderColor = pBorderColor;
|
||||
}
|
||||
|
||||
getBackgroundColor() {
|
||||
return this.backgroundColor;
|
||||
}
|
||||
|
||||
setBackgroundColor(pBackgroundColor) {
|
||||
this.backgroundColor = pBackgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the amount of time the timer will run for
|
||||
*/
|
||||
getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the amount of time the timer will run for
|
||||
* @param {int} pTime
|
||||
*/
|
||||
setTime(pTime) {
|
||||
this.time = pTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called to start the timer
|
||||
*/
|
||||
start() {
|
||||
this.startTime = millis();
|
||||
// framecount is a special p5 value that counts the number of frames that have passed
|
||||
// I am using the amount of frames passed to calculate the time, assuming that the website is running at 60q frames
|
||||
// per second
|
||||
this.timeElapsed = 0;
|
||||
this.hasStarted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be called once per frame
|
||||
* it keeps track of the amount of time passed
|
||||
*/
|
||||
tick() {
|
||||
this.timeElapsed = (millis() - this.startTime) / 1000;
|
||||
if (this.timeElapsed >= this.time) {
|
||||
this.end();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* this function is called at the end of the timer
|
||||
*/
|
||||
end() {
|
||||
this.visible = false;
|
||||
api.validateTest();
|
||||
this.timeElapsed = 0;
|
||||
this.time = 0;
|
||||
api.getTest();
|
||||
// Then this function will call all other functions necessary to complete the test
|
||||
// this will likely including changing the screen and interacting with the api
|
||||
screenManager.setScreen(new EndScreen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the timer, uses the attributes of the class as options
|
||||
*/
|
||||
draw() {
|
||||
// if the time shouldn't be rendered it quickly exits out of this method
|
||||
if (!this.visible) return;
|
||||
textAlign(LEFT);
|
||||
|
||||
// adds a border for the bar if one is needed
|
||||
if (this.border && this.bar) {
|
||||
strokeWeight(1);
|
||||
stroke(this.borderColor);
|
||||
// this doesn't use the fill function like other drawings
|
||||
// but this adds the necessary color to the border
|
||||
} else {
|
||||
noStroke();
|
||||
}
|
||||
|
||||
// draws a bar that move across the screen to show the time left
|
||||
if (this.bar) {
|
||||
fill(user.colorScheme.timerBar);
|
||||
if (this.hasStarted) {
|
||||
rect(this.y, this.x, windowWidth - windowWidth * (this.timeElapsed / this.time), this.height);
|
||||
} else {
|
||||
rect(this.y, this.x, windowWidth, this.height);
|
||||
}
|
||||
}
|
||||
|
||||
// draws the text in the corner of the screen
|
||||
noStroke();
|
||||
fill(user.colorScheme.timerText);
|
||||
if (this.hasStarted) {
|
||||
text(Math.ceil(this.time - this.timeElapsed), this.x + this.width / 6, this.y + this.height / 2);
|
||||
} else {
|
||||
text("Type A Letter To Start", this.x + this.width / 6, this.y + this.height / 2);
|
||||
}
|
||||
}
|
||||
}
|
35
public/components/user.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file This file provides a time menu class for editing the length of a test
|
||||
* @author Arlo Filley
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class provides the timer, which handles when a test starts and ends as well
|
||||
* as providing a visual element for the user to see
|
||||
*/
|
||||
class UserShower {
|
||||
constructor(x, y, height, width) {
|
||||
this.button = new Button(x, y, height, width, "");
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
draw() {
|
||||
textAlign(CENTER, CENTER);
|
||||
|
||||
this.button.draw();
|
||||
|
||||
imageMode(CENTER);
|
||||
|
||||
|
||||
tint(255, 0, 255, 126);
|
||||
image(accountIcon, this.x + this.height / 2, this.y + this.width / 2);
|
||||
|
||||
if (this.button.isPressed()) {
|
||||
screenManager.setScreen(new AccountScreen());
|
||||
}
|
||||
}
|
||||
}
|
27
public/index.css
Executable file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Index.css
|
||||
Description: This file is the stylesheet for the html that the
|
||||
user will see if they do not have javascript enabled.
|
||||
Author: Arlo Filley
|
||||
*/
|
||||
|
||||
:root {
|
||||
--background-color: #dde83d;
|
||||
--text-color: #000000;
|
||||
--font: Verdana, Geneva, Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
noscript {
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
font-family: var(--font);
|
||||
color: var(--text-color);
|
||||
font-size: large;
|
||||
}
|
51
public/index.html
Executable file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TypeFast</title>
|
||||
|
||||
<!-- CSS Files -->
|
||||
<link rel="stylesheet" href="index.css">
|
||||
|
||||
<!-- Favicon Files -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./assets/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="./assets/favicon/site.webmanifest">
|
||||
|
||||
<!-- Main Script Files -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.min.js"></script> -->
|
||||
<script src="./index.js" type="text/javascript"></script>
|
||||
|
||||
<!-- Element Script Files -->
|
||||
<script src="./components/button.js" defer="true"></script>
|
||||
<script src="./components/canvas.js" defer="true"></script>
|
||||
<script src="./components/textbox.js" defer="true"></script>
|
||||
<script src="./components/timemenu.js" defer="true"></script>
|
||||
<script src="./components/timer.js" defer="true"></script>
|
||||
<script src="./components/menu.js" defer="true"></script>
|
||||
<script src="./components/user.js" defer="true"></script>
|
||||
|
||||
<!-- Screen Files-->
|
||||
<script src="./screens/screenmanager.js" defer="true"></script>
|
||||
<script src="./screens/startscreen.js" defer="true"></script>
|
||||
<script src="./screens/testscreen.js" defer="true"></script>
|
||||
<script src="./screens/endscreen.js" defer="true"></script>
|
||||
<script src="./screens/accountScreen.js" defer="true"></script>
|
||||
<script src="./screens/signUpScreen.js" defer="true"></script>
|
||||
<script src="./screens/loginscreen.js" defer="true"></script>
|
||||
<script src="./screens/profilescreen.js" defer="true"></script>
|
||||
<script src="./screens/leaderboardscreen.js" defer="true"></script>
|
||||
<script src="./screens/settingsScreen.js" defer="true"></script>
|
||||
|
||||
<!-- API Script Files -->
|
||||
<script src="./api/api.js" defer="true"></script>
|
||||
<script src="./api/user.js" defer="true"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Please Enable Javascript</noscript>
|
||||
</body>
|
||||
</html>
|
68
public/index.js
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file This files is the root of the website.
|
||||
* @author Arlo Filley
|
||||
*/
|
||||
|
||||
// these are all of the globally accessible variables that are
|
||||
// needed for the site to run correctly
|
||||
let canvas, api, screenManager, user;
|
||||
|
||||
/**
|
||||
* loads the any assets before the setup function
|
||||
* this allows p5.js to acess these assets including: sprites,
|
||||
* fonts, etc
|
||||
*/
|
||||
function preload() {
|
||||
roboto = loadFont('./assets/fonts/RobotoMono-Medium.ttf');
|
||||
accountIcon = loadImage('./assets/icons/account_circle.svg');
|
||||
}
|
||||
|
||||
/**
|
||||
* defines variables and sets up the p5.js canvas
|
||||
* ready to be drawn with using the draw() function
|
||||
*/
|
||||
function setup() {
|
||||
canvas = new Canvas();
|
||||
canvas.resize();
|
||||
canvas.center();
|
||||
|
||||
frameRate(60);
|
||||
|
||||
api = new API();
|
||||
screenManager = new ScreenManager();
|
||||
user = new User();
|
||||
screenManager.setScreen(new StartScreen());
|
||||
|
||||
api.login(null, null, true);
|
||||
api.getTest();
|
||||
textFont(roboto);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* called once per frame. draws all other elements onto the canvas
|
||||
* mostly will just call the screenManager.draw() method to make
|
||||
* sure that the correct screen is being drawn
|
||||
*/
|
||||
function draw() {
|
||||
background(user.colorScheme.background);
|
||||
screenManager.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* called whenever a key is pressed, the variable key contains the
|
||||
* key that the user last pressed
|
||||
*/
|
||||
function keyPressed() {
|
||||
screenManager.letterTyped(key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* called whenever the user resizes the window. Uses methods from the canvas wrapper class
|
||||
* to resize and center the canvas such that it displays correctly
|
||||
*/
|
||||
function windowResized() {
|
||||
canvas.resize();
|
||||
canvas.center();
|
||||
}
|
115
public/screens/accountScreen.js
Executable file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @file This file provides a screen for the user to login through
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - move into an seperated account page with signup and logout
|
||||
* - make passwords not display plain text
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class displays a number of textboxes that allows the user to input a
|
||||
* username and password. Then find out the user_id of that account through the
|
||||
* necessary api routes.
|
||||
*/
|
||||
class AccountScreen {
|
||||
constructor() {
|
||||
this.textboxes = [
|
||||
new Textbox(
|
||||
120, 350, 500, 100, 0, true, "#000", false,
|
||||
"#000", "#000", true
|
||||
),
|
||||
|
||||
new Textbox(
|
||||
120, 500, 500, 100, 0, true, "#000", false,
|
||||
"#000", "#000", false, false, true
|
||||
)
|
||||
]
|
||||
|
||||
this.buttons = [
|
||||
new Button(windowWidth / 2 - 400, 300, 500, 100, "", false, true, "#000", "#000", "#fff", "#000", "#000", "#fff"),
|
||||
new Button(windowWidth / 2 - 400, 450, 500, 100, "", false, true, "#000", "#000", "#fff", "#000", "#000", "#fff"),
|
||||
new Button(windowWidth / 2 + 200, 300, 100, 50, "Login"),
|
||||
new Button(windowWidth / 2 + 200, 400, 100, 50, "Sign up"),
|
||||
new Button(windowWidth / 2 + 200, 500, 100, 50, "Logout"),
|
||||
]
|
||||
|
||||
this.menu = new Menu();
|
||||
|
||||
// keeps track of which textbox the user last clicked on
|
||||
this.activeTextBox = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the SignUpScreen class with all
|
||||
* appropriate elements
|
||||
*/
|
||||
draw() {
|
||||
textSize(100);
|
||||
fill(user.colorScheme.text);
|
||||
text("Account", 0, 100, windowWidth, 110);
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw();
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.textboxes.length; i++) {
|
||||
this.textboxes[i].draw();
|
||||
}
|
||||
|
||||
textSize(30);
|
||||
text("Username", windowWidth / 2 - 400, 275);
|
||||
text("Password", windowWidth / 2 - 400, 425);
|
||||
|
||||
if (this.buttons[0].isPressed()) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=0;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=1;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (this.buttons[2].isPressed()) {
|
||||
api.login(
|
||||
this.textboxes[0].getWords(),
|
||||
this.textboxes[1].getWords()
|
||||
)
|
||||
screenManager.setScreen(new StartScreen());
|
||||
} else if (this.buttons[3].isPressed()) {
|
||||
api.createUser(
|
||||
this.textboxes[0].getWords(),
|
||||
this.textboxes[1].getWords()
|
||||
)
|
||||
screenManager.setScreen(new StartScreen());
|
||||
} else if (this.buttons[4].isPressed()) {
|
||||
api.logout();
|
||||
screenManager.setScreen(new StartScreen());
|
||||
}
|
||||
this.menu.draw();
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {key} key
|
||||
*/
|
||||
letterTyped(key) {
|
||||
if (key === "Tab" && this.activeTextBox === 0) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=1;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (key === "Tab" && this.activeTextBox === 1) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=0;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (key === "Enter") {
|
||||
api.login(
|
||||
this.textboxes[0].getWords(),
|
||||
this.textboxes[1].getWords()
|
||||
)
|
||||
screenManager.setScreen(new StartScreen());
|
||||
} else {
|
||||
this.textboxes[this.activeTextBox].letterTyped(key);
|
||||
}
|
||||
}
|
||||
}
|
36
public/screens/endscreen.js
Executable file
@ -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());
|
||||
}
|
||||
}
|
101
public/screens/leaderboardscreen.js
Executable file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @file This file provides a leaderboard for the user to compare times.
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - implement a way for the user to scroll down the tests.
|
||||
* - display more tests on the screen at once, up to 15
|
||||
* - store the leaderboard in localstorage as a cache for the most recent results
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class is a screen which shows the current leaderboard from the
|
||||
* results gotten via the api.
|
||||
*/
|
||||
class LeaderboardScreen {
|
||||
constructor() {
|
||||
this.menu = new Menu();
|
||||
api.getLeaderBoard();
|
||||
this.testButtons;
|
||||
// this.buttons = [
|
||||
// new Button(1150, 270, 240, 120, "up"),
|
||||
// new Button(1150, 390, 240, 120, "down"),
|
||||
// ]
|
||||
this.offset = 0;
|
||||
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
|
||||
}
|
||||
|
||||
draw() {
|
||||
textSize(100);
|
||||
textAlign(CENTER, CENTER);
|
||||
fill(user.colorScheme.text);
|
||||
text("Leaderboard", 0, 100, windowWidth, 120);
|
||||
this.menu.draw();
|
||||
|
||||
textSize(20);
|
||||
fill(user.colorScheme.text);
|
||||
if (user.leaderboard != undefined) {
|
||||
if (this.testButtons === undefined) {
|
||||
this.createTestButtons();
|
||||
}
|
||||
}
|
||||
|
||||
fill(user.colorScheme.testBad);
|
||||
rect(1200, 270, 20, 420);
|
||||
|
||||
fill(user.colorScheme.testGood);
|
||||
rect(1200, 270, 20, 420 / user.leaderboard.length * (this.offset + 1));
|
||||
this.scroll_bar_button.height = (user.leaderboard.length)
|
||||
|
||||
if (this.scroll_bar_button.isPressed()) {
|
||||
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
|
||||
}
|
||||
|
||||
// the furthest up the scrollbar can go is the top
|
||||
if (this.scroll_bar_button.y < 270) {
|
||||
this.scroll_bar_button.y = 270;
|
||||
}
|
||||
|
||||
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
|
||||
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
|
||||
}
|
||||
this.scroll_bar_button.draw();
|
||||
|
||||
if (this.testButtons !== undefined && this.testButtons.length > 1) {
|
||||
for (let i = 0; i < this.testButtons.length; i++) {
|
||||
this.testButtons[i][0].draw()
|
||||
this.testButtons[i][1].draw()
|
||||
this.testButtons[i][2].draw()
|
||||
}
|
||||
|
||||
this.offset = Number((
|
||||
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
|
||||
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.leaderboard.length - 13))
|
||||
).toFixed(0));
|
||||
|
||||
this.createTestButtons(this.offset)
|
||||
} else {
|
||||
fill(user.colorScheme.text);
|
||||
text("Looks Like There Isn't A Leaderboard", windowWidth / 2, 300);
|
||||
}
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
}
|
||||
|
||||
createTestButtons(offset = 0) {
|
||||
this.testButtons = [[
|
||||
new Button(400, 270, 100, 30, "ranking"), // test # button
|
||||
new Button(500, 270, 400, 30, "username"), // wpm button
|
||||
new Button(900, 270, 240, 30, "words per minute"), // accuracy button
|
||||
]];
|
||||
let j = 300;
|
||||
for (let i = 0 + offset; i < user.leaderboard.length && i <= 12+offset; i++) {
|
||||
this.testButtons.push([
|
||||
new Button(400, j, 100, 30, `${i+1}`, true, true, "#000", "#000", "#fff"), // test # button
|
||||
new Button(500, j, 400, 30, `${user.leaderboard[i].username}`, true, true, "#000", "#000", "#fff"), // accuracy button
|
||||
new Button(900, j, 240, 30, `${user.leaderboard[i].wpm}`, true, true, "#000", "#000", "#fff"), // wpm button
|
||||
])
|
||||
j+=30;
|
||||
}
|
||||
}
|
||||
}
|
108
public/screens/loginscreen.js
Executable file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file This file provides a screen for the user to login through
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - move into an seperated account page with signup and logout
|
||||
* - make passwords not display plain text
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class displays a number of textboxes that allows the user to input a
|
||||
* username and password. Then find out the user_id of that account through the
|
||||
* necessary api routes.
|
||||
*/
|
||||
class LoginScreen {
|
||||
constructor() {
|
||||
this.textboxes = [
|
||||
new Textbox(
|
||||
120, 250, 500, 100, 0, true, user.colorScheme.text, false,
|
||||
"#000", "#000", true, false
|
||||
),
|
||||
|
||||
new Textbox(
|
||||
120, 400, 500, 100, 0, true, user.colorScheme.text, false,
|
||||
"000", "#000", false, false
|
||||
)
|
||||
]
|
||||
|
||||
this.buttons = [
|
||||
new Button(
|
||||
100, 200, 500, 100, 0, true, user.colorScheme.buttonBG, false,
|
||||
"#000", "#fff", ""
|
||||
),
|
||||
|
||||
new Button(
|
||||
100, 350, 500, 100, 0, true, "#000", false,
|
||||
"#000", "#fff", ""
|
||||
),
|
||||
|
||||
new Button(
|
||||
700, 300, 100, 50, 0, true, "#000", false,
|
||||
"#000", "#00ff00", "Login"
|
||||
),
|
||||
]
|
||||
|
||||
this.menu = new Menu();
|
||||
|
||||
// keeps track of which textbox the user last clicked on
|
||||
this.activeTextBox = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the SignUpScreen class with all
|
||||
* appropriate elements
|
||||
*/
|
||||
draw() {
|
||||
fill(user.colorScheme.text);
|
||||
text("Username", 110, 175);
|
||||
text("Password", 110, 325);
|
||||
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw();
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.textboxes.length; i++) {
|
||||
this.textboxes[i].draw();
|
||||
}
|
||||
|
||||
if (this.buttons[0].isPressed()) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=0;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=1;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (this.buttons[2].isPressed()) {
|
||||
api.login(this.textboxes[0].getWords(), this.textboxes[1].getWords())
|
||||
screenManager.setScreen(new StartScreen());
|
||||
}
|
||||
|
||||
this.menu.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {key} key
|
||||
*/
|
||||
letterTyped(key) {
|
||||
if (key === "Tab" && this.activeTextBox === 0) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox = 1;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (key === "Tab" && this.activeTextBox === 1) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox = 0;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (key === "Enter") {
|
||||
api.login(
|
||||
this.textboxes[0].getWords(),
|
||||
this.textboxes[1].getWords()
|
||||
)
|
||||
screenManager.setScreen(new StartScreen());
|
||||
} else {
|
||||
this.textboxes[this.activeTextBox].letterTyped(key);
|
||||
}
|
||||
}
|
||||
}
|
111
public/screens/profilescreen.js
Executable file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @file This file provides the user with their profilescreen, where they can see their own tests
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - change button name
|
||||
* - provide filters for tests
|
||||
* - implement a way to scroll through tests
|
||||
* - create a way to have personal bests and track them
|
||||
* - store tests in localstorage.
|
||||
* - show user tests even if they are not logged in
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class displays all of the test data for a given user
|
||||
*/
|
||||
class ProfileScreen {
|
||||
constructor() {
|
||||
this.menu = new Menu();
|
||||
api.getUserTests();
|
||||
this.testButtons;
|
||||
// this.buttons = [
|
||||
// new Button(950, 270, 240, 120, "up"),
|
||||
// new Button(950, 390, 240, 120, "down"),
|
||||
// ]
|
||||
this.offset = 0;
|
||||
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
|
||||
}
|
||||
|
||||
draw() {
|
||||
textSize(100);
|
||||
textAlign(CENTER, CENTER);
|
||||
fill(user.colorScheme.text);
|
||||
text("Profile", 0, 100, windowWidth, 120);
|
||||
|
||||
this.menu.draw();
|
||||
|
||||
textSize(20);
|
||||
fill(user.colorScheme.text);
|
||||
if (user.tests != undefined) {
|
||||
if (this.testButtons === undefined) {
|
||||
this.createTestButtons();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.testButtons !== undefined && this.testButtons.length > 1) {
|
||||
for (let i = 0; i < this.testButtons.length; i++) {
|
||||
this.testButtons[i][0].draw()
|
||||
this.testButtons[i][1].draw()
|
||||
this.testButtons[i][2].draw()
|
||||
this.testButtons[i][3].draw()
|
||||
}
|
||||
} else {
|
||||
fill(user.colorScheme.text);
|
||||
text("Looks Like You Don't have any tests :(", windowWidth / 2, 300);
|
||||
}
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
|
||||
if (user.tests === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
fill(user.colorScheme.testBad);
|
||||
rect(1200, 270, 20, 420);
|
||||
|
||||
fill(user.colorScheme.testGood);
|
||||
rect(1200, 270, 20, 420 / user.tests.length * (this.offset + 1));
|
||||
this.scroll_bar_button.height = (user.tests.length)
|
||||
|
||||
if (this.scroll_bar_button.isPressed()) {
|
||||
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
|
||||
}
|
||||
|
||||
|
||||
// the furthest up the scrollbar can go is the top
|
||||
if (this.scroll_bar_button.y < 270) {
|
||||
this.scroll_bar_button.y = 270;
|
||||
}
|
||||
|
||||
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
|
||||
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
|
||||
}
|
||||
this.scroll_bar_button.draw();
|
||||
|
||||
this.offset = Number((
|
||||
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
|
||||
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.tests.length - 13))
|
||||
).toFixed(0));
|
||||
this.createTestButtons(this.offset);
|
||||
}
|
||||
|
||||
createTestButtons(offset = 0) {
|
||||
this.testButtons = [[
|
||||
new Button(600, 270, 100, 30, "test #"), // test # button
|
||||
new Button(700, 270, 100, 30, "wpm"), // wpm button
|
||||
new Button(800, 270, 100, 30, "accuracy"), // accuracy button
|
||||
new Button(900, 270, 240, 30, "characters typed")
|
||||
]];
|
||||
let j = 300;
|
||||
for (let i = user.tests.length-1-offset; i >= user.tests.length-13-offset && i >= 0; i--) {
|
||||
this.testButtons.push([
|
||||
new Button(600, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button
|
||||
new Button(700, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button
|
||||
new Button(800, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button
|
||||
new Button(900, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff")
|
||||
])
|
||||
j+=30;
|
||||
}
|
||||
}
|
||||
}
|
41
public/screens/screenmanager.js
Executable file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
public/screens/settingsScreen.js
Executable file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file This file provides a screen where the user can edit the settings of their tests
|
||||
* @author Arlo Filley
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class provides all of the necessary settings for the user to be able to edit test settings
|
||||
*/
|
||||
class SettingsScreen {
|
||||
constructor() {
|
||||
this.menu = new Menu();
|
||||
this.timeMenu = new TimeMenu();
|
||||
}
|
||||
|
||||
draw() {
|
||||
textAlign(CENTER, CENTER);
|
||||
|
||||
textSize(100);
|
||||
fill(user.colorScheme.text);
|
||||
text("Test Settings", 0, 100, windowWidth, 110);
|
||||
|
||||
this.menu.draw();
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
text("Test Duration", windowWidth / 2 - 250, 265)
|
||||
this.timeMenu.draw();
|
||||
fill("#000");
|
||||
}
|
||||
}
|
109
public/screens/signUpScreen.js
Executable file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file This file provides a way for the user to sign up for an account
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - move into an seperated account page with signup and logout
|
||||
* - make passwords not display plain text
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class provides the textboxes and methods necessary for a user
|
||||
* to sign up for a new account, which it should then log them into
|
||||
*/
|
||||
class SignUpScreen {
|
||||
constructor() {
|
||||
this.textboxes = [
|
||||
new Textbox(
|
||||
120, 250, 500, 100,0, true, "#000", false,
|
||||
"#000", "#000", true
|
||||
),
|
||||
|
||||
new Textbox(
|
||||
120, 400, 500, 100, 0, true, "#000", false,
|
||||
"000", "#000", false
|
||||
)
|
||||
]
|
||||
|
||||
this.buttons = [
|
||||
new Button(
|
||||
100, 200, 500, 100, 0, true, "#000", false,
|
||||
"#000", "#fff", ""
|
||||
),
|
||||
|
||||
new Button(
|
||||
100, 350, 500, 100, 0, true, "#000", false,
|
||||
"#000", "#fff", ""
|
||||
),
|
||||
|
||||
new Button(
|
||||
700, 300, 100, 50, 0, true, "#000", false,
|
||||
"#000", "#00ff00", "Sign Up"
|
||||
),
|
||||
]
|
||||
|
||||
this.menu = new Menu();
|
||||
|
||||
this.activeTextBox = 0
|
||||
// keeps track of which textbox the user last clicked on
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the SignUpScreen class with all
|
||||
* appropriate elements
|
||||
*/
|
||||
draw() {
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw();
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.textboxes.length; i++) {
|
||||
this.textboxes[i].draw();
|
||||
}
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
text("Username", 110, 175);
|
||||
text("Password", 110, 325);
|
||||
|
||||
if (this.buttons[0].isPressed()) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=0;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=1;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (this.buttons[2].isPressed()) {
|
||||
api.createUser(
|
||||
this.textboxes[0].getWords(),
|
||||
this.textboxes[1].getWords()
|
||||
)
|
||||
screenManager.setScreen(new StartScreen());
|
||||
}
|
||||
this.menu.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {key} key
|
||||
*/
|
||||
letterTyped(key) {
|
||||
if (key === "Tab" && this.activeTextBox === 0) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=1;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (key === "Tab" && this.activeTextBox === 1) {
|
||||
this.textboxes[this.activeTextBox].line = false;
|
||||
this.activeTextBox=0;
|
||||
this.textboxes[this.activeTextBox].line = true;
|
||||
} else if (key === "Enter") {
|
||||
api.createUser(
|
||||
this.textboxes[0].getWords(),
|
||||
this.textboxes[1].getWords()
|
||||
)
|
||||
screenManager.setScreen(new StartScreen());
|
||||
} else {
|
||||
this.textboxes[this.activeTextBox].letterTyped(key);
|
||||
}
|
||||
}
|
||||
}
|
31
public/screens/startscreen.js
Executable file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file This file is the base screen when the user visits the site
|
||||
* @author Arlo Filley
|
||||
*/
|
||||
|
||||
/**
|
||||
* This screen class is the base screen. It provides the user with basic instructions
|
||||
* and a set of menus to navigate the site
|
||||
*/
|
||||
class StartScreen {
|
||||
constructor() {
|
||||
this.menu = new Menu();
|
||||
}
|
||||
|
||||
draw() {
|
||||
textSize(100);
|
||||
textAlign(CENTER, CENTER);
|
||||
fill(user.colorScheme.text);
|
||||
text("Press enter to start test", 0, 0, windowWidth, windowHeight);
|
||||
|
||||
this.menu.draw();
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
}
|
||||
|
||||
letterTyped(key) {
|
||||
if (key === "Enter") {
|
||||
screenManager.setScreen(new TestScreen());
|
||||
}
|
||||
}
|
||||
}
|
45
public/screens/testscreen.js
Executable file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @file This file provides the functionality for the test
|
||||
* @author Arlo Filley
|
||||
*
|
||||
* TODO:
|
||||
* - provide a button that allows the user to exit the test
|
||||
* - provide a count down to the start of a test
|
||||
* - implement menus to allow the user to control the parameters of a test
|
||||
*/
|
||||
|
||||
/**
|
||||
* this class displays the text of the test to the the screen and then takes input from the user
|
||||
* displaying red if it is inaccurate, and green if it is
|
||||
*/
|
||||
class TestScreen {
|
||||
constructor() {
|
||||
this.textbox = new Textbox(
|
||||
100, windowHeight / 2 - 100,
|
||||
windowWidth - 500,windowHeight,
|
||||
0, true, "#000", false, "#000", "#000", true, true);
|
||||
this.timer = new Timer(0,0,windowWidth,50,0,true,"#fff", true, "#000", "#666", user.time, true);
|
||||
this.timerStarted = false;
|
||||
this.stopButton = new Button(0,50,200,50, "Stop Test");
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.textbox.draw();
|
||||
this.timer.draw();
|
||||
if (this.timerStarted) {
|
||||
this.timer.tick();
|
||||
}
|
||||
this.stopButton.draw();
|
||||
if (this.stopButton.isPressed()) {
|
||||
screenManager.setScreen(new StartScreen())
|
||||
}
|
||||
}
|
||||
|
||||
letterTyped(key) {
|
||||
this.textbox.letterTyped(key);
|
||||
if (!this.timerStarted) {
|
||||
this.timer.start();
|
||||
this.timerStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
2
scripts/build.sh
Normal file
@ -0,0 +1,2 @@
|
||||
#/bin/sh
|
||||
docker build -t "typing-website:Dockerfile" .
|
2
scripts/run.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#/bin/bash
|
||||
docker run -p 8000:8000 -v /Users/arlo/Code/CS-Coursework/database/dev/database.sqlite:/app/database/dev/database.sqlite -v /Users/arlo/Code/CS-Coursework/public:/app/public "typing-website:Dockerfile" \
|
40
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<Build> {
|
||||
rocket::build()
|
||||
.attach(CORS)
|
||||
// .attach(CORS)
|
||||
// testing only, should return "Hello world"
|
||||
.mount("/test", routes![test])
|
||||
// hosts the api routes necessary for the website
|
||||
|
@ -10,8 +10,10 @@
|
||||
//! - create structure for the input of post test
|
||||
|
||||
// Imports for json handling and rusqlite
|
||||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
|
||||
use rocket::serde::Serialize;
|
||||
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
|
||||
use rocket::serde::{ Serialize, json::Json };
|
||||
|
||||
use crate::typing::test::PostTest;
|
||||
|
||||
/// Contains the database connection pool
|
||||
pub struct Database(SqlitePool);
|
||||
@ -60,11 +62,23 @@ impl Database {
|
||||
|
||||
/// takes necessary data about a test and creates
|
||||
/// a database record with the data
|
||||
pub async fn create_test(&self, test_type: &str, test_length: u32, test_time: u32, test_seed: i64, quote_id: i32, wpm: u8, accuracy: u8, user_id: u32) -> Result<(), sqlx::Error> {
|
||||
pub async fn create_test(&self, test: Json<PostTest<'_>>) -> Result<(), sqlx::Error> {
|
||||
// Test to see whether the secret is correct
|
||||
let user = sqlx::query!("
|
||||
Select secret
|
||||
From Users
|
||||
Where user_id=?",
|
||||
test.user_id
|
||||
).fetch_one(&self.0).await?;
|
||||
|
||||
if user.secret != test.secret {
|
||||
return Err(sqlx::Error::RowNotFound)
|
||||
}
|
||||
|
||||
sqlx::query!("
|
||||
INSERT INTO Tests (test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id)
|
||||
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id
|
||||
test.test_type, test.test_length, test.test_time, test.test_seed, test.quote_id, test.wpm, test.accuracy, test.user_id
|
||||
).execute(&self.0).await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -17,23 +17,25 @@ use std::{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct PostTest<'r> {
|
||||
test_type: &'r str,
|
||||
test_length: u32,
|
||||
test_time: u32,
|
||||
test_seed: i64,
|
||||
quote_id: i32,
|
||||
wpm: u8,
|
||||
accuracy: u8,
|
||||
user_id: u32
|
||||
pub test_type: &'r str,
|
||||
pub test_length: u32,
|
||||
pub test_time: u32,
|
||||
pub test_seed: i64,
|
||||
pub quote_id: i32,
|
||||
pub wpm: u8,
|
||||
pub accuracy: u8,
|
||||
pub user_id: u32,
|
||||
pub secret: &'r str
|
||||
}
|
||||
|
||||
/// Api Route that accepts test data and posts it to the database
|
||||
/// Acessible from http://url/api/post_test
|
||||
#[post("/post_test", data = "<test>")]
|
||||
pub async fn create_test(test: Json<PostTest<'_>>, database: &State<Database>) {
|
||||
match database.create_test(test.test_type, test.test_length, test.test_time, test.test_seed, test.quote_id, test.wpm, test.accuracy, test.user_id).await {
|
||||
let user_id = test.user_id;
|
||||
match database.create_test(test).await {
|
||||
Err(why) => { println!("A database error occured creating a test, {why}"); }
|
||||
Ok(()) => { println!("Successfully created test for {}", test.user_id); }
|
||||
Ok(()) => { println!("Successfully created test for {user_id}"); }
|
||||
}
|
||||
}
|
||||
|
||||
|
26
websites/Typing/Admin/Delete User/index.html
Executable file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel</title>
|
||||
|
||||
<link rel="stylesheet" href="../index.css">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<p>Arlo Filley</p>
|
||||
<ul>
|
||||
<li><a href="../">Home</a></li>
|
||||
<li><a href="./">Delete A User</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h1>Password</h1>
|
||||
<input id="password" type="password">
|
||||
<table>
|
||||
<tbody id="table"></tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
51
websites/Typing/Admin/Delete User/index.js
Executable file
@ -0,0 +1,51 @@
|
||||
get();
|
||||
|
||||
function get() {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `https://arlofilley.com/api/typing/leaderboard`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.responseType = 'json';
|
||||
xhr.send();
|
||||
xhr.onload = () => {
|
||||
json = xhr.response
|
||||
createTable(json);
|
||||
};
|
||||
}
|
||||
|
||||
function createTable(pJson) {
|
||||
|
||||
let table = document.getElementById("table");
|
||||
console.log(password)
|
||||
|
||||
pJson.forEach(element => {
|
||||
let tr = document.createElement('tr');
|
||||
let button = document.createElement('button');
|
||||
let username = element.username;
|
||||
button.textContent = "delete"
|
||||
button.addEventListener( "click", () => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
let password = document.getElementById("password").value;
|
||||
xhr.open('GET', `https://arlofilley.com/api/typing/delete_user/${password}/${username}/120932187`);
|
||||
xhr.send();
|
||||
})
|
||||
|
||||
create_table_element(tr, "", [element.username, element.wpm], button)
|
||||
table.appendChild(tr)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function create_table_element(tr, string, elements, button) {
|
||||
if (elements.length > 0) {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let td = document.createElement('td');
|
||||
td.appendChild(document.createTextNode(elements[i]));
|
||||
tr.appendChild(td);
|
||||
}
|
||||
} else {
|
||||
let td = document.createElement('td');
|
||||
td.appendChild(document.createTextNode(string));
|
||||
tr.appendChild(td);
|
||||
}
|
||||
tr.appendChild(button);
|
||||
}
|
125
websites/Typing/Admin/index.css
Executable file
@ -0,0 +1,125 @@
|
||||
:root {
|
||||
--navbar: #333333;
|
||||
|
||||
/* Navbar Button Properties */
|
||||
--navbar-button: #058ED9;
|
||||
--navbar-button-hover: #283F3B;
|
||||
--navbar-button-border: #058ED9;
|
||||
|
||||
/* */
|
||||
--background: #1f1f1f;
|
||||
|
||||
/* Table Properties*/
|
||||
--table: #FFD07B;
|
||||
--table-top:#FDB833;
|
||||
|
||||
/* Title Properties */
|
||||
--title-background: #333333;
|
||||
--title-text: #ffffff;
|
||||
|
||||
/* General Properties */
|
||||
--text: #000000;
|
||||
--border: #000000;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: #333;
|
||||
height: 75px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
nav p {
|
||||
margin-right: 700px;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 38px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
padding: 10px 20px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-left: 5%;
|
||||
margin-bottom: 0%;
|
||||
color: #fff;
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-left: 5%;
|
||||
margin-top: 1.5%;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 90%;
|
||||
border-collapse: collapse;
|
||||
margin: 0px;
|
||||
margin-left: 5%;
|
||||
margin-top: 30px;
|
||||
padding: 0px;
|
||||
border-spacing: 0px;
|
||||
border: 2px var(--border) solid;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
table tr:nth-child(n) {
|
||||
background: var(--table);
|
||||
}
|
||||
|
||||
table tr:nth-child(1) {
|
||||
background: var(--table-top);
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 1px black dotted;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 40%;
|
||||
padding: 8px;
|
||||
padding-left: 12.5%;
|
||||
padding-right: 12.5%;
|
||||
}
|
20
websites/Typing/Admin/index.html
Executable file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel</title>
|
||||
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<p>Arlo Filley</p>
|
||||
<ul>
|
||||
<li><a href="./">Home</a></li>
|
||||
<li><a href="./Delete User/">Delete A User</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
@ -15,12 +15,7 @@
|
||||
*/
|
||||
class API {
|
||||
|
||||
constructor() {
|
||||
// this.url = "https://arlofilley.com/api/";
|
||||
this.url = "../api";
|
||||
// this is the url of the server
|
||||
// this may have to change later on
|
||||
}
|
||||
constructor() { this.url = "../api"; }
|
||||
|
||||
/**
|
||||
* This takes the validated data and makes a post
|
||||
@ -43,7 +38,8 @@ class API {
|
||||
'quote_id': pQuoteId,
|
||||
'wpm': pWpm,
|
||||
'accuracy': pAccuracy,
|
||||
'user_id': pUserId
|
||||
'user_id': pUserId,
|
||||
'secret': user.secret
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
@ -210,13 +206,8 @@ class API {
|
||||
* @returns
|
||||
*/
|
||||
login(pUsername, pPassword, initial = false) {
|
||||
// Variable Validation
|
||||
if (pUsername == undefined || pPassword == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// If Local Storage has the information we need there is no need to make a request to the server
|
||||
if (localStorage.getItem("username") == pUsername) {
|
||||
if (localStorage.getItem("username") === pUsername || (initial && localStorage.length === 3) ) {
|
||||
user.userId = localStorage.getItem("userId");
|
||||
user.secret = localStorage.getItem("secret");
|
||||
user.username = localStorage.getItem("username");
|
||||
@ -224,6 +215,11 @@ class API {
|
||||
return
|
||||
}
|
||||
|
||||
// Variable Validation
|
||||
if (pUsername == undefined || pPassword == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `${this.url}/login/${pUsername}/${pPassword}`);
|
||||
xhr.send();
|
||||
@ -231,7 +227,7 @@ class API {
|
||||
let response = JSON.parse(xhr.response);
|
||||
|
||||
// If there is an error with the login we need
|
||||
if (xhr.response == null) {
|
||||
if (xhr.response === null) {
|
||||
alert("Error Logging in, maybe check your password");
|
||||
return
|
||||
}
|
||||
@ -249,7 +245,7 @@ class API {
|
||||
logout() {
|
||||
user = new User();
|
||||
user.username = "no one";
|
||||
user.password = "none";
|
||||
user.password = "";
|
||||
user.userId = 0;
|
||||
user.tests = [];
|
||||
localStorage.clear();
|
||||
|
8
websites/Typing/api/user.js
Normal file → Executable file
@ -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"
|
||||
}
|
||||
|
0
websites/Typing/assets/favicon/android-chrome-192x192.png
Normal file → Executable file
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
0
websites/Typing/assets/favicon/android-chrome-512x512.png
Normal file → Executable file
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
0
websites/Typing/assets/favicon/apple-touch-icon.png
Normal file → Executable file
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
0
websites/Typing/assets/favicon/favicon-16x16.png
Normal file → Executable file
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 552 B |
0
websites/Typing/assets/favicon/favicon-32x32.png
Normal file → Executable file
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 968 B |
0
websites/Typing/assets/favicon/favicon.ico
Normal file → Executable file
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
0
websites/Typing/assets/favicon/site.webmanifest
Normal file → Executable file
0
websites/Typing/assets/fonts/RobotoMono-Medium.ttf
Normal file → Executable file
0
websites/Typing/index.css
Normal file → Executable file
0
websites/Typing/index.html
Normal file → Executable file
@ -32,7 +32,7 @@ function setup() {
|
||||
user = new User();
|
||||
screenManager.setScreen(new StartScreen());
|
||||
|
||||
api.login();
|
||||
api.login(null, null, true);
|
||||
api.getTest();
|
||||
textFont(roboto);
|
||||
}
|
||||
|
0
websites/Typing/lib/p5.js
Normal file → Executable file
0
websites/Typing/lib/p5.min.js
vendored
Normal file → Executable file
0
websites/Typing/screens/accountScreen.js
Normal file → Executable file
0
websites/Typing/screens/endscreen.js
Normal file → Executable file
51
websites/Typing/screens/leaderboardscreen.js
Normal file → Executable file
@ -17,11 +17,12 @@ class LeaderboardScreen {
|
||||
this.menu = new Menu();
|
||||
api.getLeaderBoard();
|
||||
this.testButtons;
|
||||
this.buttons = [
|
||||
new Button(1150, 270, 240, 120, "up"),
|
||||
new Button(1150, 390, 240, 120, "down"),
|
||||
]
|
||||
// this.buttons = [
|
||||
// new Button(1150, 270, 240, 120, "up"),
|
||||
// new Button(1150, 390, 240, 120, "down"),
|
||||
// ]
|
||||
this.offset = 0;
|
||||
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
|
||||
}
|
||||
|
||||
draw() {
|
||||
@ -39,29 +40,40 @@ class LeaderboardScreen {
|
||||
}
|
||||
}
|
||||
|
||||
fill(user.colorScheme.testBad);
|
||||
rect(1200, 270, 20, 420);
|
||||
|
||||
fill(user.colorScheme.testGood);
|
||||
rect(1200, 270, 20, 420 / user.leaderboard.length * (this.offset + 1));
|
||||
this.scroll_bar_button.height = (user.leaderboard.length)
|
||||
|
||||
if (this.scroll_bar_button.isPressed()) {
|
||||
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
|
||||
}
|
||||
|
||||
// the furthest up the scrollbar can go is the top
|
||||
if (this.scroll_bar_button.y < 270) {
|
||||
this.scroll_bar_button.y = 270;
|
||||
}
|
||||
|
||||
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
|
||||
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
|
||||
}
|
||||
this.scroll_bar_button.draw();
|
||||
|
||||
if (this.testButtons !== undefined && this.testButtons.length > 1) {
|
||||
for (let i = 0; i < this.testButtons.length; i++) {
|
||||
this.testButtons[i][0].draw()
|
||||
this.testButtons[i][1].draw()
|
||||
this.testButtons[i][2].draw()
|
||||
}
|
||||
if (this.buttons[0].isPressed()) {
|
||||
if (this.offset > 0) {
|
||||
this.offset--;
|
||||
}
|
||||
|
||||
this.createTestButtons(this.offset);
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
if (this.offset < user.leaderboard.length - 13) {
|
||||
this.offset++;
|
||||
}
|
||||
this.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);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw();
|
||||
}
|
||||
this.createTestButtons(this.offset)
|
||||
} else {
|
||||
fill(user.colorScheme.text);
|
||||
text("Looks Like There Isn't A Leaderboard", windowWidth / 2, 300);
|
||||
@ -78,7 +90,6 @@ class LeaderboardScreen {
|
||||
]];
|
||||
let j = 300;
|
||||
for (let i = 0 + offset; i < user.leaderboard.length && i <= 12+offset; i++) {
|
||||
console.log(i);
|
||||
this.testButtons.push([
|
||||
new Button(400, j, 100, 30, `${i+1}`, true, true, "#000", "#000", "#fff"), // test # button
|
||||
new Button(500, j, 400, 30, `${user.leaderboard[i].username}`, true, true, "#000", "#000", "#fff"), // accuracy button
|
||||
|
0
websites/Typing/screens/loginscreen.js
Normal file → Executable file
72
websites/Typing/screens/profilescreen.js
Normal file → Executable file
@ -19,11 +19,12 @@ class ProfileScreen {
|
||||
this.menu = new Menu();
|
||||
api.getUserTests();
|
||||
this.testButtons;
|
||||
this.buttons = [
|
||||
new Button(950, 270, 240, 120, "up"),
|
||||
new Button(950, 390, 240, 120, "down"),
|
||||
]
|
||||
// this.buttons = [
|
||||
// new Button(950, 270, 240, 120, "up"),
|
||||
// new Button(950, 390, 240, 120, "down"),
|
||||
// ]
|
||||
this.offset = 0;
|
||||
this.scroll_bar_button = new Button(1200, 200, 20, 20, "")
|
||||
}
|
||||
|
||||
draw() {
|
||||
@ -49,21 +50,6 @@ class ProfileScreen {
|
||||
this.testButtons[i][2].draw()
|
||||
this.testButtons[i][3].draw()
|
||||
}
|
||||
if (this.buttons[0].isPressed()) {
|
||||
if (this.offset > 0) {
|
||||
this.offset--;
|
||||
}
|
||||
this.createTestButtons(this.offset);
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
if (user.tests.length - 13 - this.offset > 0) {
|
||||
this.offset++;
|
||||
}
|
||||
this.createTestButtons(this.offset);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.buttons.length; i++) {
|
||||
this.buttons[i].draw();
|
||||
}
|
||||
} else {
|
||||
fill(user.colorScheme.text);
|
||||
text("Looks Like You Don't have any tests :(", windowWidth / 2, 300);
|
||||
@ -71,22 +57,54 @@ class ProfileScreen {
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
text(`Logged in as ${user.username}`, windowWidth-150, 15);
|
||||
|
||||
if (user.tests === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
fill(user.colorScheme.testBad);
|
||||
rect(1200, 270, 20, 420);
|
||||
|
||||
fill(user.colorScheme.testGood);
|
||||
rect(1200, 270, 20, 420 / user.tests.length * (this.offset + 1));
|
||||
this.scroll_bar_button.height = (user.tests.length)
|
||||
|
||||
if (this.scroll_bar_button.isPressed()) {
|
||||
this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2;
|
||||
}
|
||||
|
||||
|
||||
// the furthest up the scrollbar can go is the top
|
||||
if (this.scroll_bar_button.y < 270) {
|
||||
this.scroll_bar_button.y = 270;
|
||||
}
|
||||
|
||||
if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) {
|
||||
this.scroll_bar_button.y = 690 - this.scroll_bar_button.height;
|
||||
}
|
||||
this.scroll_bar_button.draw();
|
||||
|
||||
this.offset = Number((
|
||||
// number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset
|
||||
(this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.tests.length - 13))
|
||||
).toFixed(0));
|
||||
this.createTestButtons(this.offset);
|
||||
}
|
||||
|
||||
createTestButtons(offset = 0) {
|
||||
this.testButtons = [[
|
||||
new Button(400, 270, 100, 30, "test #"), // test # button
|
||||
new Button(500, 270, 100, 30, "wpm"), // wpm button
|
||||
new Button(600, 270, 100, 30, "accuracy"), // accuracy button
|
||||
new Button(700, 270, 240, 30, "characters typed")
|
||||
new Button(600, 270, 100, 30, "test #"), // test # button
|
||||
new Button(700, 270, 100, 30, "wpm"), // wpm button
|
||||
new Button(800, 270, 100, 30, "accuracy"), // accuracy button
|
||||
new Button(900, 270, 240, 30, "characters typed")
|
||||
]];
|
||||
let j = 300;
|
||||
for (let i = user.tests.length-1-offset; i >= user.tests.length-13-offset && i >= 0; i--) {
|
||||
this.testButtons.push([
|
||||
new Button(400, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button
|
||||
new Button(500, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button
|
||||
new Button(600, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button
|
||||
new Button(700, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff")
|
||||
new Button(600, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button
|
||||
new Button(700, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button
|
||||
new Button(800, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button
|
||||
new Button(900, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff")
|
||||
])
|
||||
j+=30;
|
||||
}
|
||||
|
0
websites/Typing/screens/screenmanager.js
Normal file → Executable file
5
websites/Typing/screens/settingsScreen.js
Normal file → Executable file
@ -17,9 +17,12 @@ class settingsScreen {
|
||||
|
||||
textSize(100);
|
||||
fill(user.colorScheme.text);
|
||||
text("Test Settings", 450, 100);
|
||||
text("Test Settings", windowWidth / 2, 100);
|
||||
|
||||
this.menu.draw();
|
||||
|
||||
fill(user.colorScheme.text);
|
||||
text("Test Duration", windowWidth / 2 - 250, 265)
|
||||
this.timeMenu.draw();
|
||||
fill("#000");
|
||||
text(`Logged in as ${user.username}`, windowWidth-150, 15);
|
||||
|
0
websites/Typing/screens/signUpScreen.js
Normal file → Executable file
0
websites/Typing/screens/startscreen.js
Normal file → Executable file
10
websites/Typing/screens/testscreen.js
Normal file → Executable file
@ -14,9 +14,13 @@
|
||||
*/
|
||||
class TestScreen {
|
||||
constructor() {
|
||||
this.textbox = new Textbox(100,100,windowWidth - 200,windowHeight,0,true,"#000", false, "#000", "#000", true, true);
|
||||
this.textbox = new Textbox(
|
||||
100, windowHeight / 2 - 100,
|
||||
windowWidth - 500,windowHeight,
|
||||
0, true, "#000", false, "#000", "#000", true, true);
|
||||
this.timer = new Timer(0,0,windowWidth,50,0,true,"#fff", true, "#000", "#666", user.time, true);
|
||||
this.timerStarted = false;
|
||||
this.stopButton = new Button(0,50,200,50, "Stop Test");
|
||||
}
|
||||
|
||||
draw() {
|
||||
@ -25,6 +29,10 @@ class TestScreen {
|
||||
if (this.timerStarted) {
|
||||
this.timer.tick();
|
||||
}
|
||||
this.stopButton.draw();
|
||||
if (this.stopButton.isPressed()) {
|
||||
screenManager.setScreen(new StartScreen())
|
||||
}
|
||||
}
|
||||
|
||||
letterTyped(key) {
|
||||
|
1
websites/Typing/ui_elements/button.js
Normal file → Executable file
@ -179,6 +179,7 @@ class Button {
|
||||
return;
|
||||
}
|
||||
textSize(20);
|
||||
textAlign(CENTER, CENTER);
|
||||
|
||||
if (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height) {
|
||||
|
||||
|
0
websites/Typing/ui_elements/canvas.js
Normal file → Executable file
10
websites/Typing/ui_elements/menu.js
Normal file → Executable file
@ -13,11 +13,11 @@
|
||||
class Menu {
|
||||
constructor() {
|
||||
this.buttons = [
|
||||
new Button(0, 0, 100, 30, "Account"),
|
||||
new Button(101, 0, 130, 30, "Test Data"),
|
||||
new Button(232, 0, 140, 30, "Start Test"),
|
||||
new Button(373, 0, 140, 30, "Leaderboard"),
|
||||
new Button(514, 0, 180, 30, "Test Settings")
|
||||
new Button(0, 0, 200, 50, "Account"),
|
||||
new Button(201, 0, 200, 50, "Test Data"),
|
||||
new Button(402, 0, 200, 50, "Start Test"),
|
||||
new Button(603, 0, 200, 50, "Leaderboard"),
|
||||
new Button(804, 0, 200, 50, "Test Settings")
|
||||
]
|
||||
}
|
||||
|
||||
|
18
websites/Typing/ui_elements/textbox.js
Normal file → Executable file
@ -27,6 +27,9 @@ class Textbox {
|
||||
* @param {bool} pBorder
|
||||
* @param {hexcode} pBorderColor
|
||||
* @param {hexcode} pBackgroundColor
|
||||
* @param {bool} pLine
|
||||
* @param {bool} pIsTest
|
||||
* @param {bool} pIsPassword
|
||||
*/
|
||||
constructor(
|
||||
pX, pY,
|
||||
@ -73,6 +76,7 @@ class Textbox {
|
||||
|
||||
this.goodColor = user.colorScheme.testGood;
|
||||
this.badColor = user.colorScheme.testBad;
|
||||
this.textColor = user.colorScheme.text;
|
||||
}
|
||||
|
||||
getX() {
|
||||
@ -271,7 +275,9 @@ class Textbox {
|
||||
} else {
|
||||
fill("#00044");
|
||||
}
|
||||
|
||||
text(this.testContent[this.currentLine-1][x], i, j);
|
||||
|
||||
i += 13;
|
||||
}
|
||||
j+= 30;
|
||||
@ -289,10 +295,18 @@ class Textbox {
|
||||
} else {
|
||||
fill(this.textColor);
|
||||
}
|
||||
|
||||
text(this.testContent[this.currentLine][x], i, j);
|
||||
|
||||
if (this.letters.length > 0 && x == this.letters.length && this.line) {
|
||||
fill(this.textColor)
|
||||
rect(i, j-15, 1, 30)
|
||||
}
|
||||
i += 13;
|
||||
}
|
||||
|
||||
|
||||
|
||||
i = this.x;
|
||||
j += 30;
|
||||
|
||||
@ -343,4 +357,8 @@ class Textbox {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_line(line, y) {
|
||||
|
||||
}
|
||||
}
|
24
websites/Typing/ui_elements/timemenu.js
Normal file → Executable file
@ -15,13 +15,14 @@
|
||||
class TimeMenu {
|
||||
constructor() {
|
||||
this.buttons = [
|
||||
new Button(100, 230, 100, 30, "15s"),
|
||||
new Button(100, 260, 100, 30, "30s"),
|
||||
new Button(100, 290, 100, 30, "45s"),
|
||||
new Button(100, 320, 100, 30, "60s"),
|
||||
new Button(900, 250, 100, 30, "15s"),
|
||||
new Button(900, 280, 100, 30, "30s"),
|
||||
new Button(900, 310, 100, 30, "45s"),
|
||||
new Button(900, 340, 100, 30, "60s"),
|
||||
];
|
||||
|
||||
this.topButton = this.buttons[0];
|
||||
this.dropDownButton = new Button(1000, 250, 30, 30, "v")
|
||||
this.dropdown = false;
|
||||
}
|
||||
|
||||
@ -33,29 +34,28 @@ class TimeMenu {
|
||||
|
||||
if (this.buttons[0].isPressed() && user.time != 15) {
|
||||
user.time = 15;
|
||||
this.topButton = new Button(100, 230, 100, 30, "15s");
|
||||
this.topButton = new Button(900, 250, 100, 30, "15s");
|
||||
this.dropdown = false;
|
||||
} else if (this.buttons[1].isPressed()) {
|
||||
user.time = 30;
|
||||
this.topButton = new Button(100, 230, 100, 30, "30s");
|
||||
this.topButton = new Button(900, 250, 100, 30, "30s");
|
||||
this.dropdown = false;
|
||||
} else if (this.buttons[2].isPressed()) {
|
||||
user.time = 45;
|
||||
this.topButton = new Button(100, 230, 100, 30, "45s");
|
||||
this.topButton = new Button(900, 250, 100, 30, "45s");
|
||||
this.dropdown = false;
|
||||
} else if (this.buttons[3].isPressed()) {
|
||||
user.time = 60;
|
||||
this.topButton = new Button(100, 230, 100, 30, "60s");
|
||||
this.topButton = new Button(900, 250, 100, 30, "60s");
|
||||
this.dropdown = false;
|
||||
}
|
||||
} else {
|
||||
this.topButton.draw();
|
||||
}
|
||||
|
||||
if (this.topButton.isPressed()) {
|
||||
this.dropDownButton.draw();
|
||||
if (this.dropDownButton.isPressed()) {
|
||||
this.dropdown = true;
|
||||
} else if (mouseIsPressed) {
|
||||
this.dropdown = false;
|
||||
}
|
||||
} else if (mouseIsPressed) { this.dropdown = false };
|
||||
}
|
||||
}
|