Improved leaderboard functionality and documentation
This commit is contained in:
parent
a611ab02ef
commit
caa177571b
@ -1,3 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@ -21,11 +22,11 @@
|
||||
</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>
|
||||
<ul>
|
||||
<li><a href="./create_user.html">Create User</a></li>
|
||||
<li><a href="./get_user_tests.html">Get User Tests</a></li>
|
||||
<li><a href="./leaderboard.html">Leaderboard</a></li>
|
||||
<li><a href="./login.html">Login</a></li>
|
||||
<li><a href="./create_test.html">Post Test</a></li>
|
||||
<li><a href="./create_user.html">POST <code>/api/Create_User</code></a></li>
|
||||
<li><a href="./create_test.html">POST <code>/api/Post Test</code></a></li>
|
||||
<li><a href="./get_user_tests.html">GET <code>/api/Get_User_Tests</code></a></li>
|
||||
<li><a href="./leaderboard.html">GET <code>/api/Leaderboard</code></a></li>
|
||||
<li><a href="./login.html">GET <code>/api/Login</code></a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
@ -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)
|
||||
- [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)
|
||||
|
@ -1,3 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@ -19,29 +20,48 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<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">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>
|
||||
<pre><code>GET /api/leaderboard
|
||||
</code></pre>
|
||||
<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>
|
||||
<p>This endpoint does not require any request parameters.</p>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<div class="markdown-heading"><h2 class="heading-element">GET <code>/api/leaderboard</code>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<ul>
|
||||
<li>
|
||||
<code>200 OK</code>: Successfully retrieves the leaderboard data.</li>
|
||||
<li>
|
||||
<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>[
|
||||
{
|
||||
<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">"wpm"</span>: <span class="pl-c1">85</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">"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>,
|
||||
}
|
||||
]</pre></div>
|
||||
{
|
||||
<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">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>user2<span class="pl-pds">"</span></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>
|
||||
<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>
|
||||
</html>
|
@ -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
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 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.
|
30
public/leaderboard/index.html
Normal file
30
public/leaderboard/index.html
Normal 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>
|
22
public/leaderboard/script.js
Normal file
22
public/leaderboard/script.js
Normal 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);
|
56
public/leaderboard/styles.css
Normal file
56
public/leaderboard/styles.css
Normal 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;
|
||||
}
|
||||
}
|
@ -183,7 +183,7 @@ impl Database {
|
||||
_user_id: u32,
|
||||
) -> Result<Vec<LeaderBoardTest>, 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,
|
||||
}
|
||||
|
25
src/cors.rs
Normal file
25
src/cors.rs
Normal 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"));
|
||||
}
|
||||
}
|
67
src/main.rs
67
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,
|
||||
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<Build> {
|
||||
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])
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user