diff --git a/documentation/index.html b/documentation/index.html index f20feb6..aa49121 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -1,3 +1,4 @@ + @@ -21,11 +22,11 @@

Links

\ No newline at end of file diff --git a/documentation/index.md b/documentation/index.md index fafa3b8..4e5348d 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -1,7 +1,7 @@ # Links -- [Create User](./create_user.md) -- [Get User Tests](./get_user_tests.md) -- [Leaderboard](./leaderboard.md) -- [Login](./login.md) -- [Post Test](./create_test.md) \ No newline at end of file +- [POST `/api/Create_User`](./create_user.md) +- [POST `/api/Post_Test`](./create_test.md) +- [GET `/api/Get_User_Tests`](./get_user_tests.md) +- [GET `/api/Leaderboard`](./leaderboard.md) +- [GET `/api/Login`](./login.md) diff --git a/documentation/leaderboard.html b/documentation/leaderboard.html index 71de656..e2c4045 100644 --- a/documentation/leaderboard.html +++ b/documentation/leaderboard.html @@ -1,3 +1,4 @@ + @@ -19,29 +20,48 @@ } } -

Leaderboard

-

This API endpoint retrieves the highest test data from each user and returns it as a JSON array.

-

Endpoint

-
GET /api/leaderboard
-        
-

Request Parameters

-

This endpoint does not require any request parameters.

-

Example Request

-
curl -X GET "https://example.com/api/leaderboard"
-

Response

+

Leaderboard API Endpoint

+

GET /api/leaderboard +

+

Returns the highest test data from each user as a JSON array. The data includes metrics such as username, words per minute (WPM), accuracy percentage, the time taken for the test, and the length of the test for a comprehensive overview of user performance.

+

Responses

+ +

Example Response

[
-            {
-                "userName": "user_1",
-                "wpm": 85,
-            },
-            {
-                "userName": "user_2",
-                "score": 80,
-            },
-            {
-                "userName": "user_3",
-                "wpm": 73,
-            }
-        ]
+ { + "username": "user1", + "wpm": 75, + "accuracy": 97, + "test_time": 120, + "test_length": 250 + }, + { + "username": "user2", + "wpm": 73, + "accuracy": 95, + "test_time": 115, + "test_length": 240 + } +] +

Fields

+ \ No newline at end of file diff --git a/documentation/leaderboard.md b/documentation/leaderboard.md index 0ebc29b..a4d14ae 100644 --- a/documentation/leaderboard.md +++ b/documentation/leaderboard.md @@ -1,38 +1,40 @@ -# Leaderboard +# Leaderboard API Endpoint -This API endpoint retrieves the highest test data from each user and returns it as a JSON array. +## GET `/api/leaderboard` -## Endpoint +Returns the highest test data from each user as a JSON array. The data includes metrics such as username, words per minute (WPM), accuracy percentage, the time taken for the test, and the length of the test for a comprehensive overview of user performance. -``` -GET /api/leaderboard -``` +## Responses -## Request Parameters +- `200 OK`: Successfully retrieves the leaderboard data. +- `404 Not Found`: Indicates that the leaderboard was not found. +- `500 Internal Server Error`: Indicates an issue with accessing the database. -This endpoint does not require any request parameters. - -## Example Request - -```bash -curl -X GET "https://example.com/api/leaderboard" -``` - -## Response +## Example Response ```json [ - { - "userName": "user_1", - "wpm": 85, - }, - { - "userName": "user_2", - "score": 80, - }, - { - "userName": "user_3", - "wpm": 73, - } + { + "username": "user1", + "wpm": 75, + "accuracy": 97, + "test_time": 120, + "test_length": 250 + }, + { + "username": "user2", + "wpm": 73, + "accuracy": 95, + "test_time": 115, + "test_length": 240 + } ] -``` \ No newline at end of file +``` + +## Fields + +- `username`: The name of the user. +- `wpm`: Words per minute, indicating the typing speed. +- `accuracy`: The accuracy of the user's typing, in percentage. +- `test_time`: The total time taken to complete the test, in seconds. +- `test_length`: The length of the test, typically measured in number of words. \ No newline at end of file diff --git a/public/leaderboard/index.html b/public/leaderboard/index.html new file mode 100644 index 0000000..14e6cb3 --- /dev/null +++ b/public/leaderboard/index.html @@ -0,0 +1,30 @@ + + + + + + Leaderboard + + + +

Leaderboard

+ + + + + + + + + + + + + + +
UsernameWPMAccuracy (%)Test Time (s)Test Length (characters)
+ + + + + diff --git a/public/leaderboard/script.js b/public/leaderboard/script.js new file mode 100644 index 0000000..0b78b2c --- /dev/null +++ b/public/leaderboard/script.js @@ -0,0 +1,22 @@ +async function fetchLeaderboardData() { + const response = await fetch('/api/leaderboard'); + if (!response.ok) { + console.error('Failed to fetch leaderboard data'); + return; + } + const data = await response.json(); + const tableBody = document.getElementById('leaderboardTable').getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; // Clear existing rows + data.forEach(item => { + const row = tableBody.insertRow(); + row.insertCell(0).innerText = item.username; + row.insertCell(1).innerText = item.wpm; + row.insertCell(2).innerText = item.accuracy; + row.insertCell(3).innerText = item.test_time; + row.insertCell(4).innerText = item.test_length; + }); +} + +// Ensure this function is called on page load and when the refresh button is clicked. +document.addEventListener('DOMContentLoaded', fetchLeaderboardData); +document.getElementById('refreshButton').addEventListener('click', fetchLeaderboardData); diff --git a/public/leaderboard/styles.css b/public/leaderboard/styles.css new file mode 100644 index 0000000..361e575 --- /dev/null +++ b/public/leaderboard/styles.css @@ -0,0 +1,56 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-top: 20px; +} +table { + width: 60%; + border-collapse: collapse; + margin-top: 20px; +} +th, td { + border: 1px solid #ddd; + text-align: left; + padding: 8px; +} +th { + background-color: #f2f2f2; +} +tr:nth-child(even) { + background-color: #f9f9f9; +} +.refresh-button { + cursor: pointer; + border: none; + background-color: transparent; + display: flex; + align-items: center; + color: #4CAF50; +} + +.refresh-button:hover { + transform: rotate(90deg); +} + +/* Dark Mode styles */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #e0e0e0; + } + table { + border-color: #424242; + } + th { + background-color: #333; + } + tr:nth-child(even) { + background-color: #2a2a2a; + } + .refresh-icon { + fill: #90caf9; + } +} diff --git a/src/api/sql.rs b/src/api/sql.rs index c264175..e1edf3a 100644 --- a/src/api/sql.rs +++ b/src/api/sql.rs @@ -183,7 +183,7 @@ impl Database { _user_id: u32, ) -> Result, sqlx::Error> { let tests = sqlx::query!( - "SELECT users.username, tests.wpm + "SELECT users.username, tests.wpm, tests.accuracy, tests.test_time, tests.test_length FROM tests INNER JOIN users ON users.user_id = tests.user_id GROUP BY users.username @@ -197,6 +197,9 @@ impl Database { .map(|test| LeaderBoardTest { username: test.username.clone(), wpm: test.wpm.unwrap() as u8, + accuracy: test.accuracy.unwrap() as u8, + test_time: test.test_time.unwrap() as u32, + test_length: test.test_length.unwrap() as u32 }) .collect(); @@ -262,11 +265,21 @@ pub struct Test { accuracy: u8, } -/// struct that represents all the data that gets sent to the user -/// when they make a leaderboard request +/// Represents leaderboard data sent to the user upon request. +/// This data includes username, words per minute (WPM), accuracy percentage, +/// the time taken for the test, and the length of the test. +/// +/// - `username`: The name of the user. +/// - `wpm`: Words per minute, indicating the typing speed of the user. +/// - `accuracy`: The accuracy of the user's typing, in percentage. +/// - `test_time`: The total time taken to complete the test, in seconds. +/// - `test_length`: The length of the test, typically measured in number of words. #[derive(Serialize)] #[serde(crate = "rocket::serde")] pub struct LeaderBoardTest { username: String, wpm: u8, + accuracy: u8, + test_time: u32, + test_length: u32, } diff --git a/src/cors.rs b/src/cors.rs new file mode 100644 index 0000000..cffec3f --- /dev/null +++ b/src/cors.rs @@ -0,0 +1,25 @@ +use rocket::fairing::{Fairing, Info, Kind}; +use rocket::http::Header; +use rocket::{Request, Response}; + +pub struct CORS; + +#[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", + "GET" + )); + response.set_header(Header::new("Access-Control-Allow-Headers", "*")); + response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index be6e8aa..193e35b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,38 +6,16 @@ //! - move structures into a different file //! - find a way to make logging in more secure (password hashes?) -// use rocket::fairing::{Fairing, Info, Kind}; -// use rocket::http::Header; -// use rocket::{Request, Response}; - -// pub struct CORS; - -// #[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")); -// } -// } +mod cors; +use crate::cors::CORS; // Imports for rocket #[macro_use] extern crate rocket; use rocket::{ - fs::{relative, FileServer}, - Build, Rocket, + fs::{relative, FileServer}, + response::Redirect, + Build, Rocket }; mod api; @@ -61,34 +39,43 @@ fn test() -> &'static str { "Hello World! I'm A rocket Webserver" } +#[get("/")] +async fn typing_redirect() -> Redirect { + Redirect::to(uri!("/typing/index.html")) +} + /// The main function which builds and launches the /// webserver with all appropriate routes and fileservers #[launch] async fn rocket() -> Rocket { rocket::build() - // .attach(CORS) - // testing only, should return "Hello world" - .mount("/test", routes![test]) + // Allow external to any get API methods + .attach(CORS) + // hosts the api routes necessary for the website // to interact with the database .mount("/api/documentation", FileServer::from(relative!("documentation"))) .register("/api/documentation", catchers![documentation_not_found]) - .mount( - "/api", - routes![ - sign_up, - create_test, - login, - get_tests, - leaderboard, - new_test, - ], - ) + .mount("/api",routes![ + sign_up, + login, + + new_test, + create_test, + get_tests, + leaderboard + ]) .register("/api", catchers![api_not_found]) // hosts the fileserver + .mount("/typing", routes![typing_redirect]) .mount("/typing", FileServer::from(relative!("public"))) .register("/typing", catchers![frontend_not_found]) + // The state which allows routes to access the database .manage(Database::new().await.unwrap()) + + + // testing only, should return "Hello world" + .mount("/test", routes![test]) }