From 49e2b18e56cd3834387ba3ad150ed7ef2edaf7b5 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Thu, 12 Sep 2024 23:36:55 +0200 Subject: [PATCH] Vote + Results done --- ...f49453766684f9cdf724178c9320a12a56bac.json | 20 ++ Cargo.toml | 11 +- .../20240911124945_drop_votes_convert.sql | 2 + migrations/20240911125144_votes_convert.sql | 11 + routes.json | 4 +- src/main.rs | 235 ++++++++++++++++++ static/css/404.css | 8 + static/css/index.css | 66 ++++- static/css/results.css | 77 ++++++ static/html/404.html | 19 +- static/html/index.html | 8 +- static/html/results.html | 21 ++ static/js/index.js | 83 ++++++- static/js/results.js | 152 +++++++++++ vote.db | Bin 20480 -> 20480 bytes vote.db-shm | Bin 0 -> 32768 bytes vote.db-wal | Bin 0 -> 4152 bytes 17 files changed, 698 insertions(+), 19 deletions(-) create mode 100644 .sqlx/query-630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac.json create mode 100644 migrations/20240911124945_drop_votes_convert.sql create mode 100644 migrations/20240911125144_votes_convert.sql create mode 100644 src/main.rs create mode 100644 static/css/404.css create mode 100644 static/css/results.css create mode 100644 static/html/results.html create mode 100644 static/js/results.js create mode 100644 vote.db-shm create mode 100644 vote.db-wal diff --git a/.sqlx/query-630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac.json b/.sqlx/query-630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac.json new file mode 100644 index 0000000..063d733 --- /dev/null +++ b/.sqlx/query-630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT id name FROM players", + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac" +} diff --git a/Cargo.toml b/Cargo.toml index b1162b5..a35dde3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,17 @@ edition = "2021" [dependencies] tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"]} -hyper = { version = "1.4.1", features = ["server", "http1"] } +hyper = { version = "1.4.1", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1.8", features = ["tokio"]} -sqlx = { version = "0.8.2", features = ["runtime-tokio"] } +sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlx-sqlite", "sqlite", "sqlx-macros"] } tokio-io = "0.2.0-alpha.6" dotenvy = "0.15.7" bytes = "1.7.1" http-body-util = "0.1.2" +serde = "1.0.210" serde_json = "1.0.128" -log = "0.4.22" \ No newline at end of file +log = "0.4.22" +futures = "0.3.30" +chrono = { version = "0.4.38", features = ["alloc"]} +futures-util = "0.3.30" +h2 = "0.4.6" \ No newline at end of file diff --git a/migrations/20240911124945_drop_votes_convert.sql b/migrations/20240911124945_drop_votes_convert.sql new file mode 100644 index 0000000..cf672d1 --- /dev/null +++ b/migrations/20240911124945_drop_votes_convert.sql @@ -0,0 +1,2 @@ +-- Add migration script here +drop table if exists votes \ No newline at end of file diff --git a/migrations/20240911125144_votes_convert.sql b/migrations/20240911125144_votes_convert.sql new file mode 100644 index 0000000..c753a12 --- /dev/null +++ b/migrations/20240911125144_votes_convert.sql @@ -0,0 +1,11 @@ +create table if not exists votes +( +id integer primary key not null, +timestamp text not null, +plus_id integer not null, +plus_nickname text not null, +plus_reason text not null, +moins_id integer not null, +moins_nickname text not null, +moins_reason text not null +) diff --git a/routes.json b/routes.json index fb74a8f..53057cb 100644 --- a/routes.json +++ b/routes.json @@ -1,3 +1,5 @@ { - "/": "static/html/index.html" + "/": "static/html/index.html", + "/results": "static/html/results.html", + "/archives": "static/html/archives.html" } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a991a8d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,235 @@ +extern crate core; + +use bytes::Bytes; +use bytes::Buf; +use chrono::{DateTime, Utc}; +use dotenvy::dotenv; +use futures; +use http_body_util::{BodyExt, Full}; +use hyper::body::Incoming; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Error, Method, Request, Response, StatusCode}; +use hyper_util::rt::{TokioIo, TokioTimer}; +use serde::{Deserialize, Serialize}; +use serde_json::{from_reader, Value}; +use sqlx::sqlite::SqlitePool; +use std::collections::HashMap; +use std::env; +use std::env::vars; +use std::fs::File; +use std::io::Read; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; +use tokio::net::TcpListener; + +#[derive(Serialize, Deserialize)] +struct Player { + id: i64, + name: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Vote { + vote_plus_id: i64, + vote_plus_nickname: String, + vote_plus_reason: String, + vote_moins_id: i64, + vote_moins_nickname: String, + vote_moins_reason: String, +} +async fn service(req: Request, db: Arc>) -> Result>, Error> { + match req.method() { + &Method::GET => { get(req,db).await } + &Method::POST => { + post(req, db).await + } + _ => { Ok(Response::new(Full::new(Bytes::from("This method is unimplemented")))) } + } +} + +async fn get(req: Request, db: Arc>) -> Result>, Error> { + let path = req.uri().path(); + if path.starts_with("/static/") { + get_file(path).await + } else if path.starts_with("/data/") { + get_data(path, &req, db).await + } else { + get_page(path).await + } +} + +async fn get_page(path: &str) -> Result>, Error> { + let uri_map_path = find_in_vars("ROUTES_FILE"); + let file = File::open(uri_map_path).expect("Could not find routes"); + let map: Value = from_reader(file).expect("Could not read data"); + match &map[path] { + Value::String(s) => get_file(s.as_str()).await, + _ => not_found().await + } +} + +async fn get_file(path: &str) -> Result>, Error> { + let current_dir = env::current_dir().unwrap().clone(); + let mut path = path; + if path.starts_with(r"/") { + path = path.strip_prefix(r"/").unwrap(); + } + let file_path = current_dir.join(path); + + match File::open(&file_path) { + Ok(mut file) => { + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + let header = match file_path.extension().unwrap().to_str().unwrap() { + "js" => "text/javascript", + "html" => "text/html", + "css" => "text/css", + _ => "" + }; + Ok(Response::builder().header("content-type", header).body(Full::new(Bytes::from(buf))).unwrap()) + } + Err(_) => not_found().await + } +} + +async fn get_data(path: &str, req: &Request, db: Arc>) -> Result>, Error> { + let pool = db.clone().lock().unwrap().clone(); + match path { + "/data/players" => { + let items = sqlx::query!(r#"SELECT id, name FROM players"#).fetch_all(&pool).await.unwrap(); + let players: Vec = items.iter().map(|x| Player { id: x.id, name: x.name.clone() }).collect(); + Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&players).unwrap())))) + } + "/data/votes" => { + let votes = get_votes(req, db).await; + Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&votes).unwrap())))) + } + "/data/results" => { + let votes = get_votes(req, db).await; + let ids: Vec<[i64; 2]> = votes.iter().map(|x| [x.vote_plus_id, x.vote_moins_id]).collect(); + let mut results: Vec> = Vec::new(); + for i in 0..=1 { + let mut counts = HashMap::new(); + ids.iter().for_each(|x| { + if counts.get(&x[i]).is_none() { + counts.insert(x[i], 0); + } + *counts.get_mut(&x[i]).unwrap() += 1; + }); + results.push(counts); + } + + let mut sorted: Vec> = results.into_iter().map(|hashmap| hashmap.into_iter().collect::>()).collect(); + + sorted.iter_mut().for_each(|x| x.sort_by(|a, b| b.1.cmp(&a.1))); + + Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&sorted).unwrap())))) + } + _ => not_found().await + } +} + +async fn get_votes(req: &Request, db: Arc>) -> Vec { + let pool = db.clone().lock().unwrap().clone(); + let headers = req.headers(); + let date = match headers.get("Date-to-fetch") { + Some(date) => { + let date = date.to_str().unwrap(); + let parsed_date = date.parse::(); + if parsed_date.is_err() { + None + } else { + DateTime::from_timestamp_millis(parsed_date.unwrap()) + } + } + None => Some(DateTime::from(SystemTime::now())) + }; + if date.is_none() { + return Vec::new(); + } + let items = futures::executor::block_on(async move { + let formatted_date = format!("{}", date.unwrap().format("%d/%m/%Y")); + sqlx::query!(r#"SELECT * FROM votes WHERE timestamp = ?1 ORDER BY id"#, formatted_date).fetch_all(&pool).await.unwrap() + }); + items.iter().map(|x| Vote { + vote_plus_id: x.plus_id, + vote_plus_nickname: x.plus_nickname.clone(), + vote_plus_reason: x.plus_reason.clone(), + vote_moins_id: x.moins_id, + vote_moins_nickname: x.moins_nickname.clone(), + vote_moins_reason: x.moins_reason.clone(), + }).collect() +} + +async fn post(req: Request, db: Arc>) -> Result>, Error> { + let path = req.uri().path(); + if path != "/post" { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); + } + let body = req.into_body().collect().await.unwrap(); + let data: Result = serde_json::from_reader(body.aggregate().reader()); + if data.is_err() { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) + } + let vote = data.unwrap(); + let timestamp: DateTime = DateTime::from(SystemTime::now()); + let formatted = timestamp.format("%d/%m/%Y").to_string(); + let pool = db.clone().lock().unwrap().clone(); + let mut conn = pool.acquire().await.unwrap(); + let result = sqlx::query!(r#"INSERT INTO votes ( plus_id, plus_nickname, plus_reason, moins_id, moins_nickname, moins_reason, timestamp ) + VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 )"#, + vote.vote_plus_id, + vote.vote_plus_nickname, + vote.vote_plus_reason, + vote.vote_moins_id, + vote.vote_moins_nickname, + vote.vote_moins_reason, + formatted).execute(&mut *conn).await; + if result.is_err() { + return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Internet Error"))).unwrap()) + } + Ok(Response::builder().body(Full::new(Bytes::from("OK"))).unwrap()) +} + +async fn not_found() -> Result>, Error> { + let current_dir = env::current_dir().unwrap().clone(); + let file_path = current_dir.join("static/html/404.html"); + let mut file = File::open(file_path).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + Ok(Response::builder().status(StatusCode::NOT_FOUND).body(Full::new(Bytes::from(buf))).unwrap()) +} + +fn find_in_vars(pattern: &str) -> String { + let mut vars = vars(); + let result = vars.find(|x| x.0.contains(pattern)).expect(&format!("Could not find '{}' in environment variables", pattern)); + result.1 +} + +#[tokio::main] +async fn main() { + let _ = dotenv(); + let db_adrr = find_in_vars("DATABASE_ADRR"); + let db_pool = Arc::new(Mutex::new(SqlitePool::connect(&db_adrr).await.unwrap())); + let bind_adrr: SocketAddr = SocketAddr::from_str(find_in_vars("BIND_ADRR").as_str()).expect("Could not parse bind address"); + let listener = TcpListener::bind(bind_adrr).await.expect("Could not bind to address."); + loop { + let (tcp, _) = listener.accept().await.expect("Could not accept stream"); + let io = TokioIo::new(tcp); + let db = db_pool.clone(); + let service = service_fn(move |req| { + service(req, db.clone()) + }); + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .timer(TokioTimer::new()) + .serve_connection(io, service).await + { + println!("Failed to serve connection: {:?}", err); + } + }); + } +} \ No newline at end of file diff --git a/static/css/404.css b/static/css/404.css new file mode 100644 index 0000000..4305ef3 --- /dev/null +++ b/static/css/404.css @@ -0,0 +1,8 @@ +body { + background-color: #11111b; + text-align: center; +} + +h1, h3, p, a { + color: white; +} \ No newline at end of file diff --git a/static/css/index.css b/static/css/index.css index 43d3907..1fabf13 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -9,6 +9,7 @@ body { position: fixed; bottom: 0; width: 100%; + max-height: 50px; display: flex; background-color: #b4befe; justify-content: center; @@ -21,7 +22,16 @@ body { border-radius: 5px; color: #1e1e2e; font-family: "Segoe UI"; - font-size: 20px; + font-size: 14px; +} + +.app { + margin:auto; + display: flex; + flex-direction: column; + width: 100%; + max-width: 500px; + height: calc(100vh - 60px); } .app > h2 { @@ -30,4 +40,58 @@ body { .app > h1 { color: yellow; +} + +.app > select { + width: 100%; +} + +.app > input { + width: 100%; +} + +.app > textarea { + width: 100%; + height: 100%; + resize: vertical; +} + +.buttons > button { + font-size: 16px; + margin: 4px 0 4px 0; + padding: 5px; + background-color: cornflowerblue; + color: white; + border: none; + border-radius: 5px; +} + +.buttons { + width: 100%; + display: grid; + gap: 5px; + grid-template-columns: repeat(2, 50% [col-start]); +} + +.left { + grid-column-start: 1; + grid-column-end: 2; +} + +.right { + grid-column-start: 2; + grid-column-end: 3; +} +.confirm { + margin: auto; + width: fit-content; + text-align: center; +} + +.confirm > h1 { + color: yellow; +} + +.confirm > h3 { + color: white; } \ No newline at end of file diff --git a/static/css/results.css b/static/css/results.css new file mode 100644 index 0000000..6771ffa --- /dev/null +++ b/static/css/results.css @@ -0,0 +1,77 @@ +body { + background-color: #11111b; + padding:0; + margin:0; +} + +.footer { + margin: 0; + position: fixed; + bottom: 0; + width: 100%; + max-height: 50px; + display: flex; + background-color: #b4befe; + justify-content: center; +} + +.footer > a { + padding: 10px; + margin: 5px; + background-color: #94e2d5; + border-radius: 5px; + color: #1e1e2e; + font-family: "Segoe UI"; + font-size: 14px; +} + +.app { + margin: auto; + height: calc(100vh + 60px); + width: 100%; + max-width: 500px; +} + +.app > p { + color: white; + line-break: strict; +} + +.app > h1 { + color: yellow; +} + +.app > h2 { + color:white; +} + +.app > h3 { + color: white; +} + +.buttons > button { + font-size: 16px; + margin: 4px 0 4px 0; + padding: 5px; + background-color: cornflowerblue; + color: white; + border: none; + border-radius: 5px; +} + +.buttons { + width: 100%; + display: grid; + gap: 5px; + grid-template-columns: repeat(2, 50% [col-start]); +} + +.left { + grid-column-start: 1; + grid-column-end: 2; +} + +.right { + grid-column-start: 2; + grid-column-end: 3; +} diff --git a/static/html/404.html b/static/html/404.html index a88841c..2e9024b 100644 --- a/static/html/404.html +++ b/static/html/404.html @@ -1 +1,18 @@ -

404 NOT FOUND

\ No newline at end of file + + + + + + + + + 404 Not Found + + +

404

+

Not Found Error

+

The page you are looking for does not exists.

+ Return to home page. + + \ No newline at end of file diff --git a/static/html/index.html b/static/html/index.html index 51bd227..61f336a 100644 --- a/static/html/index.html +++ b/static/html/index.html @@ -1,12 +1,14 @@ - - + + + VOTE! - +
diff --git a/static/html/results.html b/static/html/results.html new file mode 100644 index 0000000..36da9d7 --- /dev/null +++ b/static/html/results.html @@ -0,0 +1,21 @@ + + + + + + + + Résultats + + + + +
+ + + \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js index 9007585..506a49e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,17 +1,24 @@ Vote = { vote_plus_id: null, - vote_plus_nickname: null, - vote_plus_reason: null, + vote_plus_nickname: "", + vote_plus_reason: "", vote_moins_id: null, - vote_moins_nickname: null, - vote_moins_reason: null + vote_moins_nickname: "", + vote_moins_reason: "" } -load_page(0) +let _ = load_page(0) + +async function load_page(id) { + if (get_state()) { + confirm_popup(); + return; + } + let players = await fetch("data/players").then(r => r.json()); -function load_page(id) { const header = document.createElement("h1"); const buttons = document.createElement("div"); + buttons.className = "buttons"; const vote_id = document.createElement("h2"); vote_id.textContent = "Pour qui votes-tu?" @@ -20,11 +27,19 @@ function load_page(id) { vote_nickname.textContent = "As-tu un surnom à lui donner?"; const vote_nickname_input = document.createElement("input"); vote_nickname_input.inputMode = "text"; + vote_nickname_input.maxLength = 100; const vote_reason = document.createElement("h2"); vote_reason.textContent = "Pourquoi votes-tu pour lui?"; const vote_reason_input = document.createElement("textarea"); vote_reason_input.maxLength = 1000; + players.forEach((player) => { + const player_option = document.createElement("option"); + player_option.value = player["id"]; + player_option.innerText = player["name"]; + vote_id_select.appendChild(player_option); + }) + if (id) { vote_id_select.value = Vote.vote_moins_id; vote_nickname_input.value = Vote.vote_moins_nickname; @@ -33,12 +48,24 @@ function load_page(id) { let previous = document.createElement("button"); previous.textContent = "Précédent"; previous.addEventListener("click", () => { - Vote.vote_moins_id = vote_id.value; + Vote.vote_moins_id = vote_id_select.value; Vote.vote_moins_nickname = vote_nickname_input.value; Vote.vote_moins_reason = vote_reason_input.value; - load_page(0)}); + load_page(0) + }); let submit = document.createElement("button"); submit.textContent = "A Voté"; + submit.addEventListener("click", async () => { + Vote.vote_moins_id = vote_id_select.value; + Vote.vote_moins_nickname = vote_nickname_input.value; + Vote.vote_moins_reason = vote_reason_input.value; + if (await send_vote(Vote)) { + confirm_popup(); + save_state(); + } + }) + submit.className = "right"; + previous.className = "left"; buttons.append(previous, submit); } else { vote_id_select.value = Vote.vote_plus_id; @@ -48,15 +75,51 @@ function load_page(id) { let next = document.createElement("button"); next.innerText = "Suivant"; next.addEventListener("click", () => { - Vote.vote_plus_id = vote_id.value; + Vote.vote_plus_id = vote_id_select.value; Vote.vote_plus_nickname = vote_nickname_input.value; Vote.vote_plus_reason = vote_reason_input.value; load_page(1) }); + next.className = "right"; buttons.appendChild(next); } + const div = document.getElementById("app"); div.innerHTML = ""; - div.append(header,vote_id, vote_id_select, vote_nickname, vote_nickname_input, vote_reason, vote_reason_input, buttons); + div.append(header, vote_id, vote_id_select, vote_nickname, vote_nickname_input, vote_reason, vote_reason_input, buttons); +} + +function confirm_popup() { + const div = document.getElementById("app"); + div.innerText = ""; + div.className = "confirm"; + let success = document.createElement("h1"); + success.textContent = "Merci d'avoir voté!" + let sub = document.createElement("h3"); + sub.textContent = "Ton vote a bien été prit en compte!"; + div.append(success, sub); +} +function save_state() { + // create cookie with expiration the next day. + let date = new Date(Date.now()); + date.setDate(date.getDate() + 1); + date.setHours(0, 0, 0, 0); + document.cookie = "hasvoted=true; expires=" + date.toUTCString() +} + +function get_state() { + // get the cookie if exists => already voted. + let cookie = document.cookie; + return cookie.includes("hasvoted=true"); +} +async function send_vote(vote) { + if (vote.vote_plus_id === null || vote.vote_moins_id === null) { + return false; + } + vote.vote_plus_id = parseInt(vote.vote_plus_id, 10); + vote.vote_moins_id = parseInt(vote.vote_moins_id, 10); + let body = JSON.stringify(vote); + let result = await fetch(window.location.href + "post", {method: "POST", body: body}).then(r => r.status); + return result === 200; } \ No newline at end of file diff --git a/static/js/results.js b/static/js/results.js new file mode 100644 index 0000000..08077b4 --- /dev/null +++ b/static/js/results.js @@ -0,0 +1,152 @@ +async function run() { + const votes = await fetch("data/votes").then(r => r.json()); + const players = await fetch("data/players").then(r => r.json()); + let id = 0; + if (votes.length === 0) { + show_no_votes(); + } else { + show_plus(id, votes, players);} + } + +function show_no_votes() { + const app = document.getElementById("app"); + const sorry = document.createElement("h1"); + sorry.textContent = "Désolé..."; + sorry.style.textAlign = "center"; + const expl = document.createElement("h3"); + expl.textContent = "Mais il n'y a pas de votes pour le moment."; + expl.style.textAlign = "center"; + app.append(sorry, expl); +} + +function show_plus(id, votes, players) { + const app = document.getElementById("app"); + app.innerHTML = ""; + let vote = votes[id]; + let player = players[vote["vote_plus_id"] - 1]["name"]; + let nickname = vote["vote_plus_nickname"]; + let reason = vote["vote_plus_reason"]; + let moins = document.createElement("button"); + moins.textContent = "Et en moins..." + moins.addEventListener("click", () => {show_moins(id, votes, players)}) + moins.className = "right"; + + const vote_p = document.createElement("h2"); + if (nickname === "") { + vote_p.innerHTML = `${player}`; + } + else { + vote_p.innerHTML = `${nickname} (${player})` + } + + const vote_r = document.createElement("p"); + vote_r.textContent = reason; + + const head = document.createElement("h1"); + head.innerText = "EN PLUS"; + + const buttons = document.createElement("div"); + buttons.className = "buttons"; + buttons.append(moins) + + app.append(head, vote_p, vote_r, buttons); +} + +function show_moins(id, votes, players) { + const app = document.getElementById("app"); + app.innerHTML = ""; + let vote = votes[id]; + let nickname = vote["vote_moins_nickname"]; + let reason = vote["vote_moins_reason"]; + let player = players[vote["vote_moins_id"] - 1]["name"]; + let next = document.createElement("button"); + if (id === votes.length - 1) { + next.textContent = "Résultats"; + } else { + next.textContent = "Prochain vote"; + } + next.addEventListener("click", () => { + if (id === votes.length - 1) { + let _ = show_results(players) + } else { + show_plus(id+1, votes, players) + } + }) + next.className = "right"; + + const vote_p = document.createElement("h2"); + if (nickname === "") { + vote_p.innerHTML = `${player}`; + } + else { + vote_p.innerHTML = `${nickname} (${player})` + } + + const vote_r = document.createElement("p"); + vote_r.textContent = reason; + + const head = document.createElement("h1"); + head.innerText = "EN MOINS"; + + const buttons = document.createElement("div"); + buttons.className = "buttons"; + buttons.append(next) + + app.append(head, vote_p, vote_r, buttons); +} + +async function show_results(players) { + const app = document.getElementById("app"); + app.innerHTML = "" + const res = document.createElement("h1"); + res.textContent = "Résultats"; + res.style.textAlign = "center"; + app.append(res); + const vp = document.createElement("h1"); + vp.textContent = "En plus:"; + app.append(vp); + let results = await fetch("data/results").then(r => r.json()); + let plus = results[0]; + let moins = results[1]; + let prev_score = null; + let counter = 0; + for (let i = 0; i < plus.length; i++) { + let p = plus[i]; + let player = players[p[0] - 1]["name"]; + let score = p[1]; + if (prev_score == null || score < prev_score) { + counter += 1; + prev_score = score; + const place = document.createElement("h2"); + place.textContent = `En ${counter}${counter === 1 ? "ère" : "ème"} place:`; + app.append(place); + } + const result = document.createElement("h3"); + result.textContent = `${player} avec ${p[1]} votes!`; + app.append(result); + } + const sep = document.createElement("hr"); + app.append(sep); + const vm = document.createElement("h1"); + vm.textContent = "En moins:"; + app.append(vm); + prev_score = null; + counter = 0; + for (let i = 0; i < moins.length; i++) { + let p = moins[i]; + let player = players[p[0] - 1]["name"]; + let score = p[1]; + if (prev_score == null || score < prev_score) { + counter += 1; + prev_score = score; + const place = document.createElement("h2"); + place.textContent = `En ${counter}${counter === 1 ? "ère" : "ème"} place:`; + app.append(place); + } + const result = document.createElement("h3"); + result.textContent = `${player} avec ${score} votes!`; + app.append(result); + } +} + +let _ = run(); \ No newline at end of file diff --git a/vote.db b/vote.db index cca3ffbd28496dc9aa04be35bc36682b735c1080..a03eed665ea3434a7bbb1254d1e9b210edd8952a 100644 GIT binary patch literal 20480 zcmeI4du$X%7{K>#_a3`-Z-5F%t(CbnNUJUM`ao%n+QI=XcP*4_rJ!7vy;*O2`?$N^ zqYs0R`i_bw#srP=A3`E35)Ba(i5QCUA0^@|2$7)jF!=fhlWL4QdskXGLH)-ll6RU9YEbfW4#UnW0)*g-ELXl7-s7b6EHo24Ja3-hfQf?Th$uKrd8yk5| zs}pQhCzdVg8ElgwS~#i5Xn8Afj(Lg3Q025C>EwcDIU-r6QL55S)wqbuqUvHki`ol0 z6>8J0FIZXWy05-~Bcn;fBxg+|+<8bQ3Pqtt{97!d@aM6KLhWd+%NMM#cRk=Kw3e{4 z+E7AQ@61WIYSOWlIS+|^BEZ!}s|Q>3<}X(X9&^c}It>UdjhWZ0Cg9Tio8@ z>UtNW8c8xl!O&;;gp{}GJwWq`QW%MFX$edJYJTVMCep|S?qkq46b&4w0t6rc1b_e# z00KY&2mk>f00e*l5C8)I1_6oldFD0o8aE@gYgOl#!qN(sO@ltU+l>v4&Gik7>ce3i zZftF7Y;A0s{zmA)i*K!(_hH$NmhXQ4{>;|v0!PPR8r|~r>230Jhqs@6Jbq(zhk4D~ zmQx#gS5+9Sd+pb@!umYnvrTjyOyrr)36 z8~yF7{-@qwx9{7LhxYd#(7xDHeWrKE4TpdE*mLDQYi@Jzan^KS*~+&->3a2j()(;YOdbd{Bd{eEB5*$c=Xin zvhQc_+8S){-QVx>ag{~Ksgn~cE$6##Uf9~$@Byj##w#l;$L21Yv3c(4cRyQn`@WiL z{mrF6?ELA|SKQ3o;q{x={2Ec3Iv?ilzIyL|mVM-2fnHM%ea@h9^dmY>1qeU@2mk>f z00e*l5C8%|00;m9AOHk_z@^R8!S?`U{~N2+Jc@y528)zBlI4!Py+c-94$vTqlKsrU4>BK7y7oq_@#padI|)9 z01yBIKmZ5;0U!VbfB+Bx0+$PcX=N-kuXAjBj_9}^FP1W@=w_Kv-*P8sN^rXa+;<+OT_ne@+aO2)S9sxX~pD&um>%x6@78g&vg^LC2(0@O)N%E?}qTJ%X8 z?V(?!Mo}}bT3&$`E;aLB-bpo+_fx|qlCBbi_ffH(7y~qC2(zj4WzyKowqz{N4+|ce zZ8My={R#Sx@l`At33iqe^IW z_PUZ(8(VfJgEn(_F(KE?C-30J(3CGWtq!WnKw5Je)QPIdsS#6F3=?D3Bv}Q^S#yLq z@`wV}E<+#+T-HnKJ}m5|WLD7zw6rr7DOoxz7*r}8&dE}!n`=Y<@}R%Ge98^2mC0u4whei8z{=1nGetV{ zxkQm3Q?=-l1;gYnT)d9XQx$y0oT=Jup}lNzx^&Ky2lY}H;!B;=(pipgsyN5V9|s8X Awg3PC delta 292 zcmZozz}T>Wae}lUGXnzyD-go~^F$qEQDz3cssdjA9}FzKEDU_z{M-5Dcv&_J3QXYP zY7%E*7Z(?2Y~h_O!K*acpHJPSBr`X)xFj*RKp`cuBvm0VzeFLgG$%)gm#ZMBv^YL9 zMIke4(;j3z;{|zJ04i_9hROv1RWeXl diff --git a/vote.db-shm b/vote.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..bfbd57209fd20582f3012610ea2789d9d01e22f0 GIT binary patch literal 32768 zcmeI)u?fOZ5C-52Xm4Q=FcS!Fpp}aW7WS@?6*OC9f^=yGHwZ~yA-p!F;rrp>aU6H> z4e-kT6u%S^ySZXJ*LD9It7HFu)?FOFpT}*uoHxzAEf>ybyJO6f_tQJw6aoYY5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oPACK>FoQ2oNAZfB*pk1PBlyK!5-N0t5&U oAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0{t<8 literal 0 HcmV?d00001 diff --git a/vote.db-wal b/vote.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..89ca832f87e5f4bcdaea415a68c49110667787da GIT binary patch literal 4152 zcmeHJ%}T>S5Kh`EjSwr;QcEq}ToeWAkAeu^JSns;9Djqy|QFgKuzMYxOz_-gs)=wwTl$F7pqRc3fUEMq@#n+ef zr||AJcg-+_CL;fczdg)uJr0)VbqK1NP|1W-C!ZfkNRtdm1|$QL0m*=5Kr$d1kPQ4L z1FO@TVra#ZS}N7*mFh;NUahYY!AXmYp$pzhj5b`f2sr%2HVrLbRtt^zBeUc9y&a6a zMU!@LqUkf{hz7P0_o?S$#7uNZF*smiQV$8wbu$;KW5rN&mwTFS#J!Sj+NZ>BvuI!k zQ(_$ijzzi-Le#;WAd5Qzj&ML^**_$N$YcFe_x#Iw!S?62%@7tI>iP_Xtz)Q5H7&a| zrUf=e!lwfDK@-CJHnByE6K1n^Bt?XSDs%x2SM(AD+AJ99Y(AaM#%-X=<*q3lpW4EO zlAI38r|LSaxGIr^Oz->fF6dN2E0p8v