Vote + Results done
This commit is contained in:
20
.sqlx/query-630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac.json
generated
Normal file
20
.sqlx/query-630981b7637ee724554aa5a4ad1f49453766684f9cdf724178c9320a12a56bac.json
generated
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
futures = "0.3.30"
|
||||
chrono = { version = "0.4.38", features = ["alloc"]}
|
||||
futures-util = "0.3.30"
|
||||
h2 = "0.4.6"
|
||||
2
migrations/20240911124945_drop_votes_convert.sql
Normal file
2
migrations/20240911124945_drop_votes_convert.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add migration script here
|
||||
drop table if exists votes
|
||||
11
migrations/20240911125144_votes_convert.sql
Normal file
11
migrations/20240911125144_votes_convert.sql
Normal file
@@ -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
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"/": "static/html/index.html"
|
||||
"/": "static/html/index.html",
|
||||
"/results": "static/html/results.html",
|
||||
"/archives": "static/html/archives.html"
|
||||
}
|
||||
235
src/main.rs
Normal file
235
src/main.rs
Normal file
@@ -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<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, 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<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, 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<Response<Full<Bytes>>, 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<Response<Full<Bytes>>, 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<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, 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<Player> = 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<HashMap<i64, i64>> = 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<Vec<(i64, i64)>> = results.into_iter().map(|hashmap| hashmap.into_iter().collect::<Vec<(i64, i64)>>()).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<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Vec<Vote> {
|
||||
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::<i64>();
|
||||
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<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, 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<Vote, serde_json::Error> = 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<Utc> = 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<Response<Full<Bytes>>, 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
8
static/css/404.css
Normal file
8
static/css/404.css
Normal file
@@ -0,0 +1,8 @@
|
||||
body {
|
||||
background-color: #11111b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1, h3, p, a {
|
||||
color: white;
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -31,3 +41,57 @@ 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;
|
||||
}
|
||||
77
static/css/results.css
Normal file
77
static/css/results.css
Normal file
@@ -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;
|
||||
}
|
||||
@@ -1 +1,18 @@
|
||||
<h1>404 NOT FOUND</h1>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<base href="/" target="_top">
|
||||
<link rel="stylesheet" href="/static/css/404.css">
|
||||
<title>404 Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404</h1>
|
||||
<h3>Not Found Error</h3>
|
||||
<p>The page you are looking for does not exists.</p>
|
||||
<a href="/">Return to home page.</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="/" target="_top">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<base href="/" target="_top">
|
||||
<script src="static/js/index.js" defer="defer"></script>
|
||||
<title>VOTE!</title>
|
||||
<link rel="stylesheet" href="static/css/index.css">
|
||||
<link rel="stylesheet" href="static/css/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app"></div>
|
||||
|
||||
21
static/html/results.html
Normal file
21
static/html/results.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<base href="/" target="_top">
|
||||
<title>Résultats</title>
|
||||
<link rel="stylesheet" href="static/css/results.css">
|
||||
<script src="/static/js/results.js" defer="defer"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app"></div>
|
||||
<div id="footer" class="footer">
|
||||
<a href="/">Voter</a>
|
||||
<a href="/results">Résultats</a>
|
||||
<a href="/archives">Archives</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
152
static/js/results.js
Normal file
152
static/js/results.js
Normal file
@@ -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();
|
||||
BIN
vote.db-shm
Normal file
BIN
vote.db-shm
Normal file
Binary file not shown.
BIN
vote.db-wal
Normal file
BIN
vote.db-wal
Normal file
Binary file not shown.
Reference in New Issue
Block a user