diff --git a/.gitignore b/.gitignore index 0e4ce72..52a7778 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ Cargo.lock /target # Added by BurnyLlama -.env \ No newline at end of file +.env +static/location-data \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 02afbc8..b1afc2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] dotenvy = "0.15.7" rocket = { version = "0.5.0-rc.3", features = ["json"] } +rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] } serde = "1.0.183" sqlx = { version = "0.7.1", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid" ] } tokio-test = "0.4.2" diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..f514d6c --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,4 @@ +[default] +address = "0.0.0.0" +port = 12345 +template_dir = "templates" \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..25f984e --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,68 @@ +use rocket::{response::Redirect, Route, State}; +use rocket_dyn_templates::{context, Template}; +use uuid::Uuid; + +use crate::database::{ + models::{ + guess::Guess, + location::{self, Location}, + }, + DatabaseHandler, +}; + +#[get("/")] +fn landing() -> Template { + Template::render("landing", context! {}) +} + +#[get("/start-game")] +async fn start_game(db: &State) -> Result { + Location::get_random(db, true) + .await + .map(|location| Redirect::to(format!("/location/{}", location.id))) + .map_err(|_| { + "Sorry for this, but the game ran into an error. Please try again!".to_string() + }) +} + +#[get("/location/")] +async fn location_via_id(db: &State, id: String) -> Result { + let uuid = match Uuid::parse_str(&id) { + Ok(uuid) => uuid, + Err(_) => return Err("Sorry, that id seems invalid!".to_string()), + }; + let location = match Location::get_by_id(db, &uuid).await { + Ok(location) => location, + Err(_) => return Err("Sorry, that location seems invalid!".to_string()), + }; + + Ok(Template::render("location", context! { location })) +} + +#[get("/location/?")] +async fn location_guess_via_id( + db: &State, + id: String, + guess: String, +) -> Result { + let uuid = match Uuid::parse_str(&id) { + Ok(uuid) => uuid, + Err(_) => return Err("Sorry, that id seems invalid!".to_string()), + }; + let location = match Location::get_by_id(db, &uuid).await { + Ok(location) => location, + Err(_) => return Err("Sorry, that location seems invalid!".to_string()), + }; + + let is_guess_correct = guess == location.map; + let guess_with_results = Guess::create(guess, is_guess_correct); + + Ok(Template::render( + "location", + context! { location, guess: guess_with_results }, + )) +} + +pub fn get_all_routes() -> Vec { + routes![landing, start_game, location_via_id, location_guess_via_id] +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 13791d4..af2f140 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -5,7 +5,7 @@ use std::{env, error::Error}; pub mod models; pub struct DatabaseHandler { - pool: PgPool, + pub pool: PgPool, } impl DatabaseHandler { diff --git a/src/database/models/guess.rs b/src/database/models/guess.rs new file mode 100644 index 0000000..5ffb137 --- /dev/null +++ b/src/database/models/guess.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Guess { + pub map: String, + pub result: String, +} + +impl Guess { + pub fn create(map: String, is_correct: bool) -> Guess { + Guess { + map, + result: match is_correct { + true => "correct".to_string(), + false => "incorrect".to_string(), + }, + } + } +} diff --git a/src/database/models/location.rs b/src/database/models/location.rs index a9622fd..fe9328f 100644 --- a/src/database/models/location.rs +++ b/src/database/models/location.rs @@ -4,15 +4,31 @@ use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Location { - id: Uuid, - map: String, + pub id: Uuid, + pub map: String, + pub has_entities: bool, } impl Location { - pub async fn get_random(db: &DatabaseHandler) -> Result { + /// Get a random location. Use the `has_entities` option to specify whether the location should be returned with entities. No entities = harder. + pub async fn get_random( + db: &DatabaseHandler, + has_entities: bool, + ) -> Result { sqlx::query_as!( Location, - "SELECT id, map FROM locations ORDER BY RANDOM() LIMIT 1" + "SELECT id, map, has_entities FROM locations WHERE has_entities = $1 ORDER BY RANDOM() LIMIT 1", + has_entities + ) + .fetch_one(&db.pool) + .await + } + + pub async fn get_by_id(db: &DatabaseHandler, id: &Uuid) -> Result { + sqlx::query_as!( + Location, + "SELECT id, map, has_entities FROM locations WHERE id = $1", + id ) .fetch_one(&db.pool) .await @@ -52,7 +68,7 @@ mod tests { async fn test() { let db = get_db().await; - let location = match Location::get_random(&db).await { + let location = match Location::get_random(&db, false).await { Ok(location) => location, Err(err) => panic!("Error while getting random location! {:?}", err), }; diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index b6c6145..787f572 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -1 +1,2 @@ +pub mod guess; pub mod location; diff --git a/src/main.rs b/src/main.rs index 3354b36..0378051 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ #[macro_use] extern crate rocket; +mod app; mod database; +use app::get_all_routes; use database::DatabaseHandler; +use rocket::fs::FileServer; +use rocket_dyn_templates::Template; #[rocket::main] async fn main() { @@ -15,7 +19,14 @@ async fn main() { ), }; - match rocket::build().manage(database).launch().await { + match rocket::build() + .attach(Template::fairing()) + .manage(database) + .mount("/", get_all_routes()) + .mount("/static", FileServer::from("./static")) + .launch() + .await + { Ok(_) => (), Err(err) => println!("Encountered an error while starting rocket:\n{}", err), } diff --git a/static/ddguesser.css b/static/ddguesser.css new file mode 100644 index 0000000..8c01fb5 --- /dev/null +++ b/static/ddguesser.css @@ -0,0 +1,83 @@ +body.ddguesser { + display: grid; + grid-template-areas: 'nav' 'main'; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; +} + +body.ddguesser > nav { + background-color: #1F1F47; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +body.ddguesser > main { + display: flex; + flex-direction: column; + justify-content: center; + width: min(90ch, 90%); + place-self: start center; + margin-bottom: 5vh; +} + +header { + text-align: center; +} + +header > h1 { + font-size: 3rem; +} + +img#meow { + width: min(45ch, 50%); + aspect-ratio: 1 / 1; + margin: 0 auto 2rem auto; +} + +a#start-game { + width: max-content; + margin: 0 auto; +} + +form { + width: 100%; + display: flex; +} + +form > input[type=text] { + color: #FAFAFF; + flex-grow: 1; +} + +.guess { + font-size: 1.5rem; + /* text-align: center; */ +} + +span.correct { + color: #64DB4F; +} + +span.incorrect { + color: #E04F53; +} + +.woof { + position: relative; + width: 100%; +} + +.woof > img { + width: 100%; +} + +.woof > p { + position: absolute; + bottom: 1rem; + user-select: none; + translate: 50%; + right: 50%; + padding: 1rem 2rem; + background-color: #0008; +} \ No newline at end of file diff --git a/static/nebulosa.css b/static/nebulosa.css new file mode 100644 index 0000000..dde0d71 --- /dev/null +++ b/static/nebulosa.css @@ -0,0 +1,2 @@ +/* Generated using Nebulosa CSS. https://git.qwik.space/BurnyLlama/nebulosa-css */ +:root,*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;scroll-behavior:smooth}:root,body{font-family:"Manrope",sans-serif;line-height:1.5;color:#fafaff;background-color:#0f0f24;font-size:12pt}@font-face{font-family:"Manrope";src:local("Manrope"),url("/assets/fonts/Manrope-Variable.woff2") format("woff2");font-weight:200 800}@font-face{font-family:"Alegreya";src:local("Alegreya"),url("/assets/fonts/Alegreya-Regular.woff2") format("woff2");font-weight:400}@font-face{font-family:"Alegreya";src:local("Alegreya"),url("/assets/fonts/Alegreya-Italic.woff2") format("woff2");font-weight:400;font-style:italic}h1,h2,h3,h4,h5,h6{font-weight:200;margin:.75em 0 .25em 0}h1{font-size:3.2rem;position:relative}h1::before{content:"";position:absolute;top:0;left:0;width:5rem;height:.1rem;background-color:#eb5beb}h2{font-size:2.4rem}h3{font-size:2rem}h4{font-size:1.8rem}h5{font-size:1.6rem}h6{font-size:1.4rem}header{padding:3rem 0}header>h1{margin:0;font-size:6.4rem;font-weight:300}header>h1::before{content:none}header>h2{margin:0;font-size:4.8rem;font-weight:200}p,ul,ol{font-family:"Alegreya",serif;font-size:1rem;margin:1rem 0 .5rem 0}ul,ol{margin:0 0 1.5rem 2rem}ul>li::marker,ol>li::marker{color:#8484ae}blockquote{position:relative;font-family:"Alegreya",serif;margin:2rem 1rem;padding:1rem;background-color:#1f1f47;box-shadow:.5rem .5rem 0 0 #cecee3}blockquote::before{font-family:"Alegreya",serif;font-size:2rem;text-align:center;content:'"';position:absolute;top:-0.75rem;left:-0.75rem;height:2rem;width:2rem;border-radius:100%;color:#fafaff;background-color:#eb5beb}blockquote[cite]{margin-bottom:3rem}blockquote[cite]::after{content:"– " attr(cite);position:absolute;bottom:-2.5rem;right:3rem;color:#52527a}.btn{cursor:pointer;display:block;text-decoration:none;text-align:center;margin:.5rem 1rem .5rem 0;padding:.5rem 1rem;border:2px solid #eb5beb}.btn.raised{transform:translateY(-0.3rem);box-shadow:0 .3rem 0 0 #d943d9;transition:.3s transform,.3s box-shadow}.btn.raised:hover{transform:translateY(0);box-shadow:0 0 0 0 #d943d9}.btn.raised.primary{color:#fafaff;background-color:#eb5beb}.btn.raised.secondary{color:#fafaff;background-color:#ff4da3;border-color:#ff4da3;box-shadow:0 .3rem 0 0 #e03d8c}.btn.raised.secondary:hover{box-shadow:0 0 0 0 #e03d8c}.btn.raised[disabled]{color:#6b6b94;background-color:#cecee3;border-color:#cecee3;box-shadow:0 .3rem 0 0 #aeaed1}.btn.raised[disabled]:hover{transform:translateY(-0.3rem)}.btn.filled{transition:.3s color,.3s background-color}.btn.filled.primary{color:#fafaff;background-color:#eb5beb}.btn.filled.secondary{color:#fafaff;background-color:#ff4da3;border-color:#ff4da3}.btn.filled:hover{color:#0f0f24;background-color:rgba(0,0,0,0)}.btn.filled[disabled]{color:#6b6b94;background-color:#cecee3;border-color:#cecee3}.btn.outline{transition:.3s color,.3s background-color}.btn.outline.primary{color:#fafaff;background-color:rgba(0,0,0,0)}.btn.outline.primary:hover{background-color:#eb5beb}.btn.outline.secondary{color:#fafaff;background-color:rgba(0,0,0,0);border-color:#ff4da3}.btn.outline.secondary:hover{background-color:#ff4da3}.btn.outline[disabled]{color:#6b6b94;border-color:#cecee3}.btn.textonly{font-weight:bold;font-family:"Manrope";padding:.5rem .3rem;background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0);text-transform:uppercase}.btn.textonly.primary{color:#eb5beb}.btn.textonly.primary:hover{text-decoration:wavy underline}.btn.textonly.secondary{color:#fafaff}.btn.textonly.secondary:hover{text-decoration:wavy underline}.btn.textonly[disabled]{color:#aeaed1}.btn[disabled]{cursor:not-allowed}form{display:grid;width:min(100%,75ch);grid-template-columns:max-content min-content 1fr}form>*{font-family:"Manrope",sans-serif}form>label:not(.description){grid-column:1/2;margin-right:1rem;place-self:center end}form>label.description{margin-left:.5rem;grid-column:3/4;place-self:center start}form>input,form select,form textarea,form p.hint{grid-column:2/4}form p.hint{font-size:.75rem;margin:0 .5rem}form input[type=checkbox],form input[type=radio]{grid-column:2/3}input,textarea,select{background-color:#1f1f47;border:.1rem solid #1f1f47;margin:.5rem;padding:.5rem .75rem;font-family:"Manrope",sans-serif;font-size:1rem;transition:border-color .3s;display:block}input:hover,textarea:hover,select:hover{border-color:#eb5beb}input:active,input:focus,textarea:active,textarea:focus,select:active,select:focus{border-color:#eb5beb;outline:.1rem solid #eb5beb}input::placeholder,textarea::placeholder,select::placeholder{color:#52527a}input[type=checkbox],input[type=radio]{cursor:pointer;appearance:none;height:1rem;width:1rem;padding:.75rem;display:block;background-color:#1f1f47;border:.15rem solid #cecee3;border-radius:.5rem;position:relative;transition:color .3s,background-color .3s,border-color .3s}input[type=checkbox]:checked,input[type=radio]:checked{border-color:#eb5beb;background-color:#eb5beb}input[type=checkbox]:checked::after,input[type=radio]:checked::after{content:"✔";font-size:125%;color:#fafaff;position:absolute;left:50%;top:50%;translate:-50% -50%;transition:translate .3s,left .3s,background-color .3s}textarea{resize:vertical}input[type=radio]{border-radius:100%}input[type=radio]:checked::after{content:"";background-color:#fafaff;border-radius:100%;width:80%;height:80%;position:absolute}input[type=checkbox].switch{height:1px;width:3rem;border-radius:1rem}input[type=checkbox].switch::after{content:"";background-color:#cecee3;border-radius:100%;height:80%;aspect-ratio:1/1;position:absolute;top:50%;left:.1rem;translate:0 -50%;transition:.3s}input[type=checkbox].switch:checked::after{left:calc(100% - .1rem);translate:-100% -50%;background-color:#fafaff}input[type=color]{width:3rem;height:3rem;padding:.5rem;cursor:pointer}input[type=color]::-moz-color-swatch,input[type=color]::-webkit-color-swatch{width:100%;height:100%;border:0 none rgba(0,0,0,0)}input[type=file]{font-family:"Manrope",sans-serif;color:#6b6b94}input[type=file]::file-selector-button{font-family:"Manrope",sans-serif;transition:.3s color,.3s background-color;cursor:pointer;text-decoration:none;text-align:center;margin:.5rem 1rem .5rem 0;padding:.5rem 1rem;border:2px solid #eb5beb;color:#0f0f24;background-color:rgba(0,0,0,0)}input[type=file]::file-selector-button:hover{background-color:#eb5beb}input[type=range]{color:rgba(0,0,0,0);background-color:rgba(0,0,0,0);border:0 none rgba(0,0,0,0);padding:0}input[type=range]::-moz-range-track,input[type=range]::-webkit-slider-runnable-track{background-color:#eb5beb;height:.5rem;border-radius:.25rem}@supports selector(input[type="range"]::-moz-range-progress){input[type=range]::-moz-range-track,input[type=range]::-webkit-slider-runnable-track{background-color:#cecee3}}input[type=range]::-moz-range-thumb,input[type=range]::-webkit-slider-thumb{background-color:#fafaff;height:1.25rem;width:1.25rem;border:1px solid #aeaed1;border-radius:.75rem;cursor:pointer}input[type=range]::-moz-range-progress{background-color:#eb5beb;height:.5rem;border-radius:.25rem}body.publishing{display:grid;place-content:center}body.publishing main{margin:5vh 0;width:min(80ch,90vw)} \ No newline at end of file diff --git a/templates/landing.html.tera b/templates/landing.html.tera new file mode 100644 index 0000000..8db9dfb --- /dev/null +++ b/templates/landing.html.tera @@ -0,0 +1,11 @@ +{% extends "template/app" %} + +{% block main %} +
+

Welcome to DDGuesser!

+

The game where you guess the DDNet map!

+
+Random image of a cat, with the caption 'DDGuesser'. +Play game! +{% endblock main %} \ No newline at end of file diff --git a/templates/location.html.tera b/templates/location.html.tera new file mode 100644 index 0000000..2891cd1 --- /dev/null +++ b/templates/location.html.tera @@ -0,0 +1,20 @@ +{% extends "template/app" %} + +{% block main %} +
+

Which map is this image from?

+
+{% if guess %} +
+ +

Your guess {{ guess.map }} is {{ guess.result }}!

+
+{% else %} + +{% endif %} +
+ + + +
+{% endblock main %} \ No newline at end of file diff --git a/templates/template/app.html.tera b/templates/template/app.html.tera new file mode 100644 index 0000000..4de6566 --- /dev/null +++ b/templates/template/app.html.tera @@ -0,0 +1,27 @@ + + + + + + + + + {% block head %} + DDGuesser + {% endblock head %} + + + + +
+ {% block main %} +

Hello world!

+ {% endblock main %} +
+ + + \ No newline at end of file