Improved leaderboard functionality and documentation

This commit is contained in:
Arlo Filley 2024-03-22 23:50:10 +00:00
parent a611ab02ef
commit caa177571b
10 changed files with 262 additions and 106 deletions

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -21,11 +22,11 @@
</style> </style>
<div class="markdown-heading"><h1 class="heading-element">Links</h1><a id="user-content-links" class="anchor" aria-label="Permalink: Links" href="#links"><span aria-hidden="true" class="octicon octicon-link"></span></a></div> <div class="markdown-heading"><h1 class="heading-element">Links</h1><a id="user-content-links" class="anchor" aria-label="Permalink: Links" href="#links"><span aria-hidden="true" class="octicon octicon-link"></span></a></div>
<ul> <ul>
<li><a href="./create_user.html">Create User</a></li> <li><a href="./create_user.html">POST <code>/api/Create_User</code></a></li>
<li><a href="./get_user_tests.html">Get User Tests</a></li> <li><a href="./create_test.html">POST <code>/api/Post Test</code></a></li>
<li><a href="./leaderboard.html">Leaderboard</a></li> <li><a href="./get_user_tests.html">GET <code>/api/Get_User_Tests</code></a></li>
<li><a href="./login.html">Login</a></li> <li><a href="./leaderboard.html">GET <code>/api/Leaderboard</code></a></li>
<li><a href="./create_test.html">Post Test</a></li> <li><a href="./login.html">GET <code>/api/Login</code></a></li>
</ul> </ul>
</body> </body>
</html> </html>

View File

@ -1,7 +1,7 @@
# Links # Links
- [Create User](./create_user.md) - [POST `/api/Create_User`](./create_user.md)
- [Get User Tests](./get_user_tests.md) - [POST `/api/Post_Test`](./create_test.md)
- [Leaderboard](./leaderboard.md) - [GET `/api/Get_User_Tests`](./get_user_tests.md)
- [Login](./login.md) - [GET `/api/Leaderboard`](./leaderboard.md)
- [Post Test](./create_test.md) - [GET `/api/Login`](./login.md)

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -19,29 +20,48 @@
} }
} }
</style> </style>
<div class="markdown-heading"><h1 class="heading-element">Leaderboard</h1><a id="user-content-leaderboard" class="anchor" aria-label="Permalink: Leaderboard" href="#leaderboard"><span aria-hidden="true" class="octicon octicon-link"></span></a></div> <div class="markdown-heading"><h1 class="heading-element">Leaderboard API Endpoint</h1><a id="user-content-leaderboard-api-endpoint" class="anchor" aria-label="Permalink: Leaderboard API Endpoint" href="#leaderboard-api-endpoint"><span aria-hidden="true" class="octicon octicon-link"></span></a></div>
<p>This API endpoint retrieves the highest test data from each user and returns it as a JSON array.</p> <div class="markdown-heading"><h2 class="heading-element">GET <code>/api/leaderboard</code>
<div class="markdown-heading"><h2 class="heading-element">Endpoint</h2><a id="user-content-endpoint" class="anchor" aria-label="Permalink: Endpoint" href="#endpoint"><span aria-hidden="true" class="octicon octicon-link"></span></a></div> </h2><a id="user-content-get-apileaderboard" class="anchor" aria-label="Permalink: GET /api/leaderboard" href="#get-apileaderboard"><span aria-hidden="true" class="octicon octicon-link"></span></a></div>
<pre><code>GET /api/leaderboard <p>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.</p>
</code></pre> <div class="markdown-heading"><h2 class="heading-element">Responses</h2><a id="user-content-responses" class="anchor" aria-label="Permalink: Responses" href="#responses"><span aria-hidden="true" class="octicon octicon-link"></span></a></div>
<div class="markdown-heading"><h2 class="heading-element">Request Parameters</h2><a id="user-content-request-parameters" class="anchor" aria-label="Permalink: Request Parameters" href="#request-parameters"><span aria-hidden="true" class="octicon octicon-link"></span></a></div> <ul>
<p>This endpoint does not require any request parameters.</p> <li>
<div class="markdown-heading"><h2 class="heading-element">Example Request</h2><a id="user-content-example-request" class="anchor" aria-label="Permalink: Example Request" href="#example-request"><span aria-hidden="true" class="octicon octicon-link"></span></a></div> <code>200 OK</code>: Successfully retrieves the leaderboard data.</li>
<div class="highlight highlight-source-shell"><pre>curl -X GET <span class="pl-s"><span class="pl-pds">"</span>https://example.com/api/leaderboard<span class="pl-pds">"</span></span></pre></div> <li>
<div class="markdown-heading"><h2 class="heading-element">Response</h2><a id="user-content-response" class="anchor" aria-label="Permalink: Response" href="#response"><span aria-hidden="true" class="octicon octicon-link"></span></a></div> <code>404 Not Found</code>: Indicates that the leaderboard was not found.</li>
<li>
<code>500 Internal Server Error</code>: Indicates an issue with accessing the database.</li>
</ul>
<div class="markdown-heading"><h2 class="heading-element">Example Response</h2><a id="user-content-example-response" class="anchor" aria-label="Permalink: Example Response" href="#example-response"><span aria-hidden="true" class="octicon octicon-link"></span></a></div>
<div class="highlight highlight-source-json"><pre>[ <div class="highlight highlight-source-json"><pre>[
{ {
<span class="pl-ent">"userName"</span>: <span class="pl-s"><span class="pl-pds">"</span>user_1<span class="pl-pds">"</span></span>, <span class="pl-ent">"username"</span>: <span class="pl-s"><span class="pl-pds">"</span>user1<span class="pl-pds">"</span></span>,
<span class="pl-ent">"wpm"</span>: <span class="pl-c1">85</span>, <span class="pl-ent">"wpm"</span>: <span class="pl-c1">75</span>,
<span class="pl-ent">"accuracy"</span>: <span class="pl-c1">97</span>,
<span class="pl-ent">"test_time"</span>: <span class="pl-c1">120</span>,
<span class="pl-ent">"test_length"</span>: <span class="pl-c1">250</span>
}, },
{ {
<span class="pl-ent">"userName"</span>: <span class="pl-s"><span class="pl-pds">"</span>user_2<span class="pl-pds">"</span></span>, <span class="pl-ent">"username"</span>: <span class="pl-s"><span class="pl-pds">"</span>user2<span class="pl-pds">"</span></span>,
<span class="pl-ent">"score"</span>: <span class="pl-c1">80</span>,
},
{
<span class="pl-ent">"userName"</span>: <span class="pl-s"><span class="pl-pds">"</span>user_3<span class="pl-pds">"</span></span>,
<span class="pl-ent">"wpm"</span>: <span class="pl-c1">73</span>, <span class="pl-ent">"wpm"</span>: <span class="pl-c1">73</span>,
<span class="pl-ent">"accuracy"</span>: <span class="pl-c1">95</span>,
<span class="pl-ent">"test_time"</span>: <span class="pl-c1">115</span>,
<span class="pl-ent">"test_length"</span>: <span class="pl-c1">240</span>
} }
]</pre></div> ]</pre></div>
<div class="markdown-heading"><h2 class="heading-element">Fields</h2><a id="user-content-fields" class="anchor" aria-label="Permalink: Fields" href="#fields"><span aria-hidden="true" class="octicon octicon-link"></span></a></div>
<ul>
<li>
<code>username</code>: The name of the user.</li>
<li>
<code>wpm</code>: Words per minute, indicating the typing speed.</li>
<li>
<code>accuracy</code>: The accuracy of the user's typing, in percentage.</li>
<li>
<code>test_time</code>: The total time taken to complete the test, in seconds.</li>
<li>
<code>test_length</code>: The length of the test, typically measured in number of words.</li>
</ul>
</body> </body>
</html> </html>

View File

@ -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.
``` ## Responses
GET /api/leaderboard
```
## 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 Response
## Example Request
```bash
curl -X GET "https://example.com/api/leaderboard"
```
## Response
```json ```json
[ [
{ {
"userName": "user_1", "username": "user1",
"wpm": 85, "wpm": 75,
"accuracy": 97,
"test_time": 120,
"test_length": 250
}, },
{ {
"userName": "user_2", "username": "user2",
"score": 80,
},
{
"userName": "user_3",
"wpm": 73, "wpm": 73,
"accuracy": 95,
"test_time": 115,
"test_length": 240
} }
] ]
``` ```
## 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.

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Leaderboard</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<H1>Leaderboard</H1>
<button class="refresh-button" onclick="fetchLeaderboard()"></button>
<table id="leaderboardTable">
<thead>
<tr>
<th>Username</th>
<th>WPM</th>
<th>Accuracy (%)</th>
<th>Test Time (s)</th>
<th>Test Length (characters)</th>
</tr>
</thead>
<tbody>
<!-- Rows will be filled by JavaScript -->
</tbody>
</table>
<script src="script.js"></script>
</body>
</html>

View File

@ -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);

View File

@ -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;
}
}

View File

@ -183,7 +183,7 @@ impl Database {
_user_id: u32, _user_id: u32,
) -> Result<Vec<LeaderBoardTest>, sqlx::Error> { ) -> Result<Vec<LeaderBoardTest>, sqlx::Error> {
let tests = sqlx::query!( let tests = sqlx::query!(
"SELECT users.username, tests.wpm "SELECT users.username, tests.wpm, tests.accuracy, tests.test_time, tests.test_length
FROM tests FROM tests
INNER JOIN users ON users.user_id = tests.user_id INNER JOIN users ON users.user_id = tests.user_id
GROUP BY users.username GROUP BY users.username
@ -197,6 +197,9 @@ impl Database {
.map(|test| LeaderBoardTest { .map(|test| LeaderBoardTest {
username: test.username.clone(), username: test.username.clone(),
wpm: test.wpm.unwrap() as u8, 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(); .collect();
@ -262,11 +265,21 @@ pub struct Test {
accuracy: u8, accuracy: u8,
} }
/// struct that represents all the data that gets sent to the user /// Represents leaderboard data sent to the user upon request.
/// when they make a leaderboard 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)] #[derive(Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct LeaderBoardTest { pub struct LeaderBoardTest {
username: String, username: String,
wpm: u8, wpm: u8,
accuracy: u8,
test_time: u32,
test_length: u32,
} }

25
src/cors.rs Normal file
View File

@ -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"));
}
}

View File

@ -6,38 +6,16 @@
//! - move structures into a different file //! - move structures into a different file
//! - find a way to make logging in more secure (password hashes?) //! - find a way to make logging in more secure (password hashes?)
// use rocket::fairing::{Fairing, Info, Kind}; mod cors;
// use rocket::http::Header; use crate::cors::CORS;
// 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"));
// }
// }
// Imports for rocket // Imports for rocket
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use rocket::{ use rocket::{
fs::{relative, FileServer}, fs::{relative, FileServer},
Build, Rocket, response::Redirect,
Build, Rocket
}; };
mod api; mod api;
@ -61,34 +39,43 @@ fn test() -> &'static str {
"Hello World! I'm A rocket Webserver" "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 /// The main function which builds and launches the
/// webserver with all appropriate routes and fileservers /// webserver with all appropriate routes and fileservers
#[launch] #[launch]
async fn rocket() -> Rocket<Build> { async fn rocket() -> Rocket<Build> {
rocket::build() rocket::build()
// .attach(CORS) // Allow external to any get API methods
// testing only, should return "Hello world" .attach(CORS)
.mount("/test", routes![test])
// hosts the api routes necessary for the website // hosts the api routes necessary for the website
// to interact with the database // to interact with the database
.mount("/api/documentation", FileServer::from(relative!("documentation"))) .mount("/api/documentation", FileServer::from(relative!("documentation")))
.register("/api/documentation", catchers![documentation_not_found]) .register("/api/documentation", catchers![documentation_not_found])
.mount( .mount("/api",routes![
"/api",
routes![
sign_up, sign_up,
create_test,
login, login,
get_tests,
leaderboard,
new_test, new_test,
], create_test,
) get_tests,
leaderboard
])
.register("/api", catchers![api_not_found]) .register("/api", catchers![api_not_found])
// hosts the fileserver // hosts the fileserver
.mount("/typing", routes![typing_redirect])
.mount("/typing", FileServer::from(relative!("public"))) .mount("/typing", FileServer::from(relative!("public")))
.register("/typing", catchers![frontend_not_found]) .register("/typing", catchers![frontend_not_found])
// The state which allows routes to access the database
.manage(Database::new().await.unwrap()) .manage(Database::new().await.unwrap())
// testing only, should return "Hello world"
.mount("/test", routes![test])
} }