From 4a3f8f50040c2b2370bf949dc1277b97fe43bf7e Mon Sep 17 00:00:00 2001 From: Matthew Kaminski Date: Sun, 10 Dec 2023 02:28:49 -0500 Subject: [PATCH] Add initial commit Copied from https://github.com/vsquad-gitea/pool-elo --- .cargo/config.toml | 2 + .dockerignore | 8 +++ .gitignore | 23 +++---- Cargo.toml | 34 +++++++++++ Dockerfile | 14 +++++ LICENSE | 2 +- README.md | 53 +++++++++++++++- package.json | 16 +++++ rust-toolchain.toml | 3 + src/components/layout.rs | 51 ++++++++++++++++ src/components/mod.rs | 1 + src/data/mod.rs | 5 ++ src/data/pool_match.rs | 55 +++++++++++++++++ src/data/store.rs | 34 +++++++++++ src/data/user.rs | 3 + src/endpoints.rs | 1 + src/error_views.rs | 62 +++++++++++++++++++ src/main.rs | 71 ++++++++++++++++++++++ src/server/mod.rs | 2 + src/server/routes.rs | 34 +++++++++++ src/templates/add_game_form.rs | 101 +++++++++++++++++++++++++++++++ src/templates/global_state.rs | 44 ++++++++++++++ src/templates/index.rs | 24 ++++++++ src/templates/mod.rs | 22 +++++++ src/templates/one_v_one_board.rs | 39 ++++++++++++ src/templates/overall_board.rs | 72 ++++++++++++++++++++++ style/tailwind.css | 4 ++ tailwind.config.js | 14 +++++ 28 files changed, 777 insertions(+), 17 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 .dockerignore create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 package.json create mode 100644 rust-toolchain.toml create mode 100644 src/components/layout.rs create mode 100644 src/components/mod.rs create mode 100644 src/data/mod.rs create mode 100644 src/data/pool_match.rs create mode 100644 src/data/store.rs create mode 100644 src/data/user.rs create mode 100644 src/endpoints.rs create mode 100644 src/error_views.rs create mode 100644 src/main.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/routes.rs create mode 100644 src/templates/add_game_form.rs create mode 100644 src/templates/global_state.rs create mode 100644 src/templates/index.rs create mode 100644 src/templates/mod.rs create mode 100644 src/templates/one_v_one_board.rs create mode 100644 src/templates/overall_board.rs create mode 100644 style/tailwind.css create mode 100644 tailwind.config.js diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..5fba26e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = [ "--cfg", "engine" ] diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bcce266 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.github +.vscode +LICENSE +README.md +.gitignore +Dockerfile +node_modules +target diff --git a/.gitignore b/.gitignore index 3ca43ae..4108571 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,9 @@ -# ---> Rust -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +node_modules Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - +dist +target +static +pkg +./Cargo.lock +package-lock.json +/data/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7428fd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +resolver = "2" +name = "yugioh-inv" +version = "0.1.0" +edition = "2021" + +[dependencies] +perseus = { version = "0.4.2", features = ["hydrate"] } +sycamore = { version = "0.8.2", features = [ + "suspense", + "web", + "wasm-bindgen-interning", +] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +env_logger = "0.10.0" +log = "0.4.20" +once_cell = "1.18.0" +web-sys = "0.3.64" +cfg-if = "1.0.0" +chrono = { version = "0.4.31", features = ["serde"] } + +[target.'cfg(engine)'.dev-dependencies] +fantoccini = "0.19" + +[target.'cfg(engine)'.dependencies] +tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } +perseus-axum = { version = "0.4.2" } +axum = "0.6" +tower-http = { version = "0.3", features = ["fs"] } + +[target.'cfg(client)'.dependencies] +wasm-bindgen = "0.2" +reqwest = { version = "0.11", features = ["json"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db6fb3b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM debian:stable-slim +WORKDIR /var/www/app + +COPY pkg server + +RUN addgroup --system server && \ + usermod -a -G server www-data && \ + chown -R www-data:server /var/www/app + +USER www-data + +EXPOSE 80 + +CMD ["./server/server"] diff --git a/LICENSE b/LICENSE index 0ed93b9..762f948 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 matkam7 +Copyright (c) 2023 vsquad Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 0fcc4da..4266666 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ -# yugioh-inventory +# Installing requirements +## 1. Install rust: +### Windows: + +Download installer from https://www.rust-lang.org/tools/install + +### Unix based systems: + +Run `curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh` + +## 2. Install npm + +### Windows: + +https://nodejs.org/en + +### Unix based systems: + +`sudo apt install nodejs` +`sudo apt install npm` + +## 3. Install Perseus, for real-time updates while developing + +`cargo install perseus-cli` +`rustup target add wasm32-unknown-unknown` + +## 4. Install tailwindcss, for styling + +`npm install -D tailwindcss` + +Also take a look at + +Website: +https://framesurge.sh/perseus/en-US/ + +Simple tutorial: +https://blog.logrocket.com/building-rust-app-perseus/ + +# Building the project + +To build CSS run: +`npm run build` + +To build the project for testing, run +`perseus serve --verbose` + +# Deploying the project + +First run +`perseus deploy` + +The folder with everything necessary will be in `/pkg` diff --git a/package.json b/package.json new file mode 100644 index 0000000..0d3cb3d --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "yugioh-inv", + "version": "1.0.0", + "description": "yugioh-inv", + "main": "index.js", + "scripts": { + "build": "npx tailwindcss -i ./style/tailwind.css -o ./static/style.css", + "watch": "npx tailwindcss -i ./style/tailwind.css -o ./static/style.css --watch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "tailwindcss": "^3.3.3" + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..e918eb3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +targets = ["wasm32-unknown-unknown"] diff --git a/src/components/layout.rs b/src/components/layout.rs new file mode 100644 index 0000000..868696f --- /dev/null +++ b/src/components/layout.rs @@ -0,0 +1,51 @@ +use sycamore::prelude::*; + +#[derive(Prop)] +pub struct LayoutProps<'a, G: Html> { + pub title: &'a str, + pub children: Children<'a, G>, +} + +#[component] +pub fn Layout<'a, G: Html>( + cx: Scope<'a>, + LayoutProps { title: _, children }: LayoutProps<'a, G>, +) -> View { + let children = children.call(cx); + + view! { cx, + header { + div (class = "flex items-center justify-between") { + div (class = "w-full text-gray-700 md:text-center text-2xl font-semibold") { + "Yugioh Inventory" + } + } + + div (class = "container mx-auto px-6 py-3") { + nav (class = "sm:flex sm:justify-center sm:items-center mt-4 hidden") { + div (class = "flex flex-col sm:flex-row"){ + a(href = "add-game-form", + class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" + ) { "Add game result" } + a(href = "one-v-one-board", + class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" + ) { "1v1 Leaderboard" } + a(href = "overall-board", + class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" + ) { "Overall Leaderboard" } + } + } + } + } + + main(style = "my-8") { + div(class = "container mx-auto px-6") { + div(class = "md:flex mt-8 md:-mx-4") { + div(class = "rounded-md overflow-hidden bg-cover bg-center") { + (children) + } + } + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..dd64619 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1 @@ +pub mod layout; diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..68c233a --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,5 @@ +pub mod pool_match; +pub mod user; + +#[cfg(engine)] +pub mod store; diff --git a/src/data/pool_match.rs b/src/data/pool_match.rs new file mode 100644 index 0000000..3ee5b55 --- /dev/null +++ b/src/data/pool_match.rs @@ -0,0 +1,55 @@ +use crate::data::user::PlayerId; +use chrono::serde::ts_seconds; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +pub type MatchId = u32; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum MatchType { + Standard8Ball, + Standard9Ball, + CutThroat, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MatchData { + pub type_: MatchType, + pub winners: Vec, + pub losers: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PoolMatch { + pub id: MatchId, + pub data: MatchData, + #[serde(with = "ts_seconds")] + pub time: DateTime, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PoolMatchList { + pub pool_matches: Vec, + pub max_id: MatchId, +} + +impl PoolMatch { + pub fn new(data: MatchData, time: DateTime) -> PoolMatch { + PoolMatch { id: 0, data, time } + } +} + +impl PoolMatchList { + pub fn new() -> PoolMatchList { + PoolMatchList { + pool_matches: vec![], + max_id: 0, + } + } + + pub fn add_pool_match(&mut self, mut pool_match: PoolMatch) { + pool_match.id = self.max_id + 1; + self.max_id += 1; + self.pool_matches.push(pool_match); + } +} diff --git a/src/data/store.rs b/src/data/store.rs new file mode 100644 index 0000000..839a49b --- /dev/null +++ b/src/data/store.rs @@ -0,0 +1,34 @@ +// (Server only) In-memory data storage and persistent storage + +use crate::data::pool_match::PoolMatchList; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path, sync::Mutex}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Store { + pub matches: PoolMatchList, +} + +impl Store { + fn new() -> Store { + fs::create_dir_all("data").unwrap(); + match Path::new("data/store.json").exists() { + false => Store { + matches: PoolMatchList::new(), + }, + true => { + let contents = fs::read_to_string("data/store.json").unwrap(); + serde_json::from_str(&contents).unwrap() + } + } + } + // TODO -> Store data + #[allow(dead_code)] + pub fn write(&self) { + let contents = serde_json::to_string(&self).unwrap(); + fs::write("data/store.json", contents).unwrap(); + } +} + +pub static DATA: Lazy> = Lazy::new(|| Mutex::new(Store::new())); diff --git a/src/data/user.rs b/src/data/user.rs new file mode 100644 index 0000000..9815690 --- /dev/null +++ b/src/data/user.rs @@ -0,0 +1,3 @@ + +pub type PlayerId = u32; + diff --git a/src/endpoints.rs b/src/endpoints.rs new file mode 100644 index 0000000..5cda3da --- /dev/null +++ b/src/endpoints.rs @@ -0,0 +1 @@ +pub const MATCH: &str = "/api/post-match"; diff --git a/src/error_views.rs b/src/error_views.rs new file mode 100644 index 0000000..bf523a1 --- /dev/null +++ b/src/error_views.rs @@ -0,0 +1,62 @@ +use perseus::errors::ClientError; +use perseus::prelude::*; +use sycamore::prelude::*; + +pub fn get_error_views() -> ErrorViews { + ErrorViews::new(|cx, err, _err_info, _err_pos| { + match err { + ClientError::ServerError { status, message: _ } => match status { + 404 => ( + view! { cx, + title { "Page not found" } + }, + view! { cx, + p { "Sorry, that page doesn't seem to exist." } + }, + ), + // 4xx is a client error + _ if (400..500).contains(&status) => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { "There was something wrong with the last request, please try reloading the page." } + }, + ), + // 5xx is a server error + _ => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { "Sorry, our server experienced an internal error. Please try reloading the page." } + }, + ), + }, + ClientError::Panic(_) => ( + view! { cx, + title { "Critical error" } + }, + view! { cx, + p { "Sorry, but a critical internal error has occurred. This has been automatically reported to our team, who'll get on it as soon as possible. In the mean time, please try reloading the page." } + }, + ), + ClientError::FetchError(_) => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { "A network error occurred, do you have an internet connection? (If you do, try reloading the page.)" } + }, + ), + _ => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { (format!("An internal error has occurred: '{}'.", err)) } + }, + ), + } + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a5c1e32 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,71 @@ +mod components; +mod data; +mod endpoints; +mod error_views; +#[cfg(engine)] +mod server; +mod templates; + +use perseus::prelude::*; +use sycamore::prelude::view; + +cfg_if::cfg_if! { + if #[cfg(engine)] { + use std::net::SocketAddr; + use perseus::{ + i18n::TranslationsManager, + server::ServerOptions, + stores::MutableStore, + turbine::Turbine, + }; + use crate::server::routes::register_routes; + } +} + +#[cfg(engine)] +pub async fn dflt_server( + turbine: &'static Turbine, + opts: ServerOptions, + (host, port): (String, u16), +) { + let addr: SocketAddr = format!("{}:{}", host, port) + .parse() + .expect("Invalid address provided to bind to."); + let mut app = perseus_axum::get_router(turbine, opts).await; + + app = register_routes(app); + + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[perseus::main(dflt_server)] +pub fn main() -> PerseusApp { + env_logger::init(); + + PerseusApp::new() + .global_state_creator(crate::templates::global_state::get_global_state_creator()) + .template(crate::templates::index::get_template()) + .template(crate::templates::add_game_form::get_template()) + .template(crate::templates::one_v_one_board::get_template()) + .template(crate::templates::overall_board::get_template()) + .error_views(crate::error_views::get_error_views()) + .index_view(|cx| { + view! { cx, + html (class = "flex w-full h-full"){ + head { + meta(charset = "UTF-8") + meta(name = "viewport", content = "width=device-width, initial-scale=1.0") + // Perseus automatically resolves `/.perseus/static/` URLs to the contents of the `static/` directory at the project root + link(rel = "stylesheet", href = ".perseus/static/style.css") + } + body (class = "w-full"){ + // Quirk: this creates a wrapper `
` around the root `
` by necessity + PerseusRoot() + } + } + } + }) +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..9bebc39 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,2 @@ +pub mod routes; + diff --git a/src/server/routes.rs b/src/server/routes.rs new file mode 100644 index 0000000..934c557 --- /dev/null +++ b/src/server/routes.rs @@ -0,0 +1,34 @@ +// (Server only) Routes + +use crate::{ + data::{ + pool_match::{PoolMatch, PoolMatchList}, + store::DATA, + }, + endpoints::MATCH, +}; +use axum::{ + extract::Json, + routing::{post, Router}, +}; +use std::thread; + +pub fn register_routes(app: Router) -> Router { + let app = app.route(MATCH, post(post_match)); + app +} + +async fn post_match(Json(pool_match): Json) -> Json { + // Update the store with the new match + let matches = thread::spawn(move || { + // Get the store + let mut data = DATA.lock().unwrap(); + (*data).matches.add_pool_match(pool_match); + println!("{:?}", (*data).matches.pool_matches); + (*data).matches.clone() + }) + .join() + .unwrap(); + + Json(matches) +} diff --git a/src/templates/add_game_form.rs b/src/templates/add_game_form.rs new file mode 100644 index 0000000..0939d14 --- /dev/null +++ b/src/templates/add_game_form.rs @@ -0,0 +1,101 @@ +use crate::data::pool_match::MatchType; +use crate::{components::layout::Layout, data::pool_match::MatchData}; +use perseus::prelude::*; +use serde::{Deserialize, Serialize}; +use sycamore::prelude::*; +use web_sys::Event; + +cfg_if::cfg_if! { + if #[cfg(client)] { + use crate::data::pool_match::{PoolMatch, PoolMatchList}; + use crate::templates::global_state::AppStateRx; + use crate::endpoints::MATCH; + use crate::templates::get_api_path; + use chrono::Utc; + } +} + +// Reactive page + +#[derive(Serialize, Deserialize, Clone, ReactiveState)] +#[rx(alias = "PageStateRx")] +struct PageState { + name: String, +} + +fn add_game_form_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStateRx) -> View { + let handle_add_match = move |_event: Event| { + #[cfg(client)] + { + // state.name.get().as_ref().clone() + spawn_local_scoped(cx, async move { + let new_match = PoolMatch::new( + MatchData { + type_: MatchType::Standard8Ball, + winners: vec![1], + losers: vec![2, 3, 4], + }, + Utc::now(), + ); + let client = reqwest::Client::new(); + let new_matches = client + .post(get_api_path(MATCH).as_str()) + .json(&new_match) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + global_state.matches.set(new_matches); + }) + } + }; + + view! { cx, + Layout(title = "Add Game Results") { + div (class = "flex flex-wrap") { + input (bind:value = state.name, + class = "appearance-none block w-full bg-gray-200 text-gray-700 border \ + border-red-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none \ + focus:bg-white",) + } + div (class = "flex flex-wrap") { + button(on:click = handle_add_match, + class = "flex-shrink-0 bg-teal-500 hover:bg-teal-700 border-teal-500 \ + hover:border-teal-700 text-sm border-4 text-white py-1 px-2 rounded", + ) { + "Add result" + } + } + } + } +} + +#[engine_only_fn] +async fn get_request_state( + _info: StateGeneratorInfo<()>, + _req: Request, +) -> Result> { + Ok(PageState { + name: "Ferris".to_string(), + }) +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Add Game Form" } + } +} + +// Template + +pub fn get_template() -> Template { + Template::build("add-game-form") + .request_state_fn(get_request_state) + .view_with_state(add_game_form_page) + .head(head) + .build() +} diff --git a/src/templates/global_state.rs b/src/templates/global_state.rs new file mode 100644 index 0000000..4ac7502 --- /dev/null +++ b/src/templates/global_state.rs @@ -0,0 +1,44 @@ +// Not a page, global state that is shared between all pages + +use crate::data::pool_match::PoolMatchList; +use perseus::{prelude::*, state::GlobalStateCreator}; +use serde::{Deserialize, Serialize}; + +cfg_if::cfg_if! { + if #[cfg(engine)] { + use std::thread; + use std::ops::Deref; + use crate::data::store::DATA; + } +} + +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "AppStateRx")] +pub struct AppState { + pub matches: PoolMatchList, +} + +pub fn get_global_state_creator() -> GlobalStateCreator { + GlobalStateCreator::new() + .build_state_fn(get_build_state) + .request_state_fn(get_request_state) +} + +#[engine_only_fn] +fn get_state() -> AppState { + let matches = thread::spawn(move || DATA.lock().unwrap().deref().matches.clone()) + .join() + .unwrap(); + + AppState { matches } +} + +#[engine_only_fn] +pub async fn get_build_state() -> AppState { + get_state() +} + +#[engine_only_fn] +pub async fn get_request_state(_req: Request) -> AppState { + get_state() +} diff --git a/src/templates/index.rs b/src/templates/index.rs new file mode 100644 index 0000000..ff585f8 --- /dev/null +++ b/src/templates/index.rs @@ -0,0 +1,24 @@ +use crate::components::layout::Layout; +use perseus::prelude::*; +use sycamore::prelude::*; + +fn index_page(cx: Scope) -> View { + view! { cx, + Layout(title = "Index") { + // Anything we put in here will be rendered inside the `
` block of the layout + p { "Hello World!" } + br {} + } + } +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Index Page" } + } +} + +pub fn get_template() -> Template { + Template::build("").view(index_page).head(head).build() +} diff --git a/src/templates/mod.rs b/src/templates/mod.rs new file mode 100644 index 0000000..8e589c5 --- /dev/null +++ b/src/templates/mod.rs @@ -0,0 +1,22 @@ +pub mod add_game_form; +pub mod global_state; +pub mod index; +pub mod one_v_one_board; +pub mod overall_board; + +#[cfg(client)] +use perseus::utils::get_path_prefix_client; + +#[allow(dead_code)] +pub fn get_api_path(path: &str) -> String { + #[cfg(engine)] + { + path.to_string() + } + #[cfg(client)] + { + let origin = web_sys::window().unwrap().origin(); + let base_path = get_path_prefix_client(); + format!("{}{}{}", origin, base_path, path) + } +} diff --git a/src/templates/one_v_one_board.rs b/src/templates/one_v_one_board.rs new file mode 100644 index 0000000..cccbe82 --- /dev/null +++ b/src/templates/one_v_one_board.rs @@ -0,0 +1,39 @@ +use crate::components::layout::Layout; +use perseus::prelude::*; +use serde::{Deserialize, Serialize}; +use sycamore::prelude::*; + +#[derive(Serialize, Deserialize, Clone, ReactiveState)] +#[rx(alias = "PageStateRx")] +struct PageState {} + +fn one_v_one_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageStateRx) -> View { + view! { cx, + Layout(title = "1v1 Leaderboard") { + p { "leaderboard" } + } + } +} + +#[engine_only_fn] +async fn get_request_state( + _info: StateGeneratorInfo<()>, + _req: Request, +) -> Result> { + Ok(PageState {}) +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "1v1 Leaderboard" } + } +} + +pub fn get_template() -> Template { + Template::build("one-v-one-board") + .request_state_fn(get_request_state) + .view_with_state(one_v_one_board_page) + .head(head) + .build() +} diff --git a/src/templates/overall_board.rs b/src/templates/overall_board.rs new file mode 100644 index 0000000..5bc0f26 --- /dev/null +++ b/src/templates/overall_board.rs @@ -0,0 +1,72 @@ +use crate::{components::layout::Layout, templates::global_state::AppStateRx, data::user::PlayerId}; + +use perseus::prelude::*; +use serde::{Deserialize, Serialize}; +use sycamore::prelude::*; + +use crate::data::pool_match::PoolMatch; + +#[derive(Serialize, Deserialize, Clone, ReactiveState)] +#[rx(alias = "PageStateRx")] +struct PageState {} + +fn format_list_or_single(to_format: &Vec) -> String{ + match to_format.len() { + 1 => to_format[0].to_string(), + _ => format!("{:?}", to_format), + } +} + +fn overall_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageStateRx) -> View { + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + + view! { cx, + Layout(title = "Overall Leaderboard") { + ul { + (View::new_fragment( + global_state.matches.get() + .pool_matches + .iter() + .rev() + .map(|item: &PoolMatch| { + let game = item.clone(); + + view! { cx, + li (class = "text-blue-700", id = "ha",) { + (game.id) + (" ") + (format_list_or_single(&game.data.winners)) + (" ") + (format_list_or_single(&game.data.losers)) + } + } + }) + .collect(), + )) + } + } + } +} + +#[engine_only_fn] +async fn get_request_state( + _info: StateGeneratorInfo<()>, + _req: Request, +) -> Result> { + Ok(PageState {}) +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Overall leaderboard" } + } +} + +pub fn get_template() -> Template { + Template::build("overall-board") + .request_state_fn(get_request_state) + .view_with_state(overall_board_page) + .head(head) + .build() +} diff --git a/style/tailwind.css b/style/tailwind.css new file mode 100644 index 0000000..a90f074 --- /dev/null +++ b/style/tailwind.css @@ -0,0 +1,4 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..596829f --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,14 @@ +module.exports = { + purge: { + mode: "all", + content: [ + "./src/**/*.rs", + "./index.html", + "./src/**/*.html", + "./src/**/*.css", + ], + }, + theme: {}, + variants: {}, + plugins: [], +};