Sycamore? Sick of more? Sick of more Javascript!
This commit is contained in:
parent
0e17e870c4
commit
5f0317c0fa
|
@ -264,6 +264,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
@ -770,6 +780,26 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-net"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"gloo-utils",
|
||||||
|
"js-sys",
|
||||||
|
"pin-project",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-render"
|
name = "gloo-render"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -916,6 +946,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html-escape"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c"
|
||||||
|
dependencies = [
|
||||||
|
"utf8-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
|
@ -1109,11 +1148,16 @@ name = "lan_party_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"console_log",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"lan_party_core",
|
"lan_party_core",
|
||||||
|
"log",
|
||||||
"paste",
|
"paste",
|
||||||
|
"reqwasm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sycamore",
|
||||||
|
"sycamore-router",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
@ -1706,6 +1750,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwasm"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb"
|
||||||
|
dependencies = [
|
||||||
|
"gloo-net",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "resolv-conf"
|
name = "resolv-conf"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -2178,6 +2231,15 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slotmap"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
@ -2349,6 +2411,116 @@ version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46a7d42f5f04604dfee0076676ec1763d26eeb7c6704139913bee6651fe0cbe1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"futures",
|
||||||
|
"indexmap",
|
||||||
|
"js-sys",
|
||||||
|
"paste",
|
||||||
|
"sycamore-core",
|
||||||
|
"sycamore-futures",
|
||||||
|
"sycamore-macro",
|
||||||
|
"sycamore-reactive",
|
||||||
|
"sycamore-web",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-core"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62c65f176ac1e7e83f9b9848a5c8b33f19d90fd20cba03e6b118bcdf2857145f"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"sycamore-reactive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-futures"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a69e4f2b65a22059f711cb36adc454791248e9abdc0b6b04c5dda396098674f2"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"sycamore-reactive",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-macro"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "648b65d7362cb4c0ea969f2c387dcd88d26c3b73b852fa01b167bb36ab336b56"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-reactive"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6376b578ad32f5f3ab6943bccec906fb0e1f0258a8bedf811afdec8c3330ef80"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"bumpalo",
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"slotmap",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-router"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "123b34a150dac877d7bfae82dadfb0c586fd35a8f5fcdf1721dafa079fdc4c40"
|
||||||
|
dependencies = [
|
||||||
|
"sycamore",
|
||||||
|
"sycamore-router-macro",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-router-macro"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92914a2f809b636d245b28d8a734801ecb8ff9c4996bbe6ea4176582e12503eb"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore-web"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c29a5344ccd47e9b7d18acb810f7d9200bbf0bed90543be58ffe6e1eee132b5"
|
||||||
|
dependencies = [
|
||||||
|
"html-escape",
|
||||||
|
"indexmap",
|
||||||
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
|
"sycamore-core",
|
||||||
|
"sycamore-reactive",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.99"
|
version = "1.0.99"
|
||||||
|
@ -2781,6 +2953,12 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-width"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||||
/// # User
|
/// # User
|
||||||
///
|
///
|
||||||
/// A user that represents a person participating in the LAN party
|
/// A user that represents a person participating in the LAN party
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
|
|
@ -7,7 +7,9 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
yew = "0.19"
|
yew = "0.19"
|
||||||
|
#yew = { git = "https://github.com/yewstack/yew" }
|
||||||
yew-router = "0.16"
|
yew-router = "0.16"
|
||||||
|
#yew-router = { git = "https://github.com/yewstack/yew" }
|
||||||
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] }
|
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] }
|
||||||
lan_party_core = { path = "../core", features = ["serde"] }
|
lan_party_core = { path = "../core", features = ["serde"] }
|
||||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
|
@ -16,6 +18,15 @@ serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
yew-hooks = "0.1"
|
yew-hooks = "0.1"
|
||||||
|
#yew-hooks = { git = "https://github.com/jetli/yew-hooks.git" }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
|
sycamore = { version = "0.8.1", features = ["serde", "suspense"] }
|
||||||
|
sycamore-router = "0.8.0"
|
||||||
|
reqwasm = "0.5"
|
||||||
|
console_log = "0.2"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
||||||
<title>Yew App</title>
|
<title>Yew App</title>
|
||||||
|
|
||||||
<link rel="preload" href="/index-a37beaf9d3547068_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
<link rel="preload" href="/index-fe4ec1b3239a32ea_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
<link rel="modulepreload" href="/index-a37beaf9d3547068.js"></head>
|
<link rel="modulepreload" href="/index-fe4ec1b3239a32ea.js"></head>
|
||||||
<body class="base theme-dark bg-gray-900 text-gray-400">
|
<body class="base theme-dark bg-gray-900 text-gray-400">
|
||||||
|
|
||||||
|
|
||||||
<script type="module">import init from '/index-a37beaf9d3547068.js';init('/index-a37beaf9d3547068_bg.wasm');</script></body></html>
|
<script type="module">import init from '/index-fe4ec1b3239a32ea.js';init('/index-fe4ec1b3239a32ea_bg.wasm');</script></body></html>
|
|
@ -0,0 +1,139 @@
|
||||||
|
use lan_party_core::user::User;
|
||||||
|
use log::debug;
|
||||||
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
use web_sys::Event;
|
||||||
|
|
||||||
|
use crate::util::api_request;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
|
let users = create_signal(cx, Vec::<User>::new());
|
||||||
|
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
users.set(
|
||||||
|
api_request::<_, Vec<User>>(reqwasm::http::Method::GET, "/user", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.map(|inner| inner.unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let handle_click = move |event: Event| {
|
||||||
|
debug!("{:#?}", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
Page {
|
||||||
|
ul {
|
||||||
|
Keyed(
|
||||||
|
iterable=users,
|
||||||
|
view=|cx, user| view! { cx,
|
||||||
|
li { (user.name) }
|
||||||
|
},
|
||||||
|
key=|user| user.name.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onclick=handle_click,text="Click me".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temp
|
||||||
|
|
||||||
|
#[derive(Prop)]
|
||||||
|
pub struct PageProps<'a, G: Html> {
|
||||||
|
pub children: Children<'a, G>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Page<'a, G: Html>(cx: Scope<'a>, props: PageProps<'a, G>) -> View<G> {
|
||||||
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
div(class="max-w-7xl mx-auto") { (children) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Prop)]
|
||||||
|
pub struct Props<F: FnMut(Event)> {
|
||||||
|
pub onclick: F,
|
||||||
|
#[builder(default)]
|
||||||
|
pub text: String,
|
||||||
|
#[builder(default)]
|
||||||
|
pub icon: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: Props<F>) -> View<G> {
|
||||||
|
let mut icon_class = String::from("mdi text-lg ");
|
||||||
|
|
||||||
|
if !props.icon.is_empty() {
|
||||||
|
icon_class.push_str(&props.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
button(class="bg-gray-700 hover:bg-gray-800 text-gray-400 font-bold py-2 px-2 rounded inline-flex items-center",on:click=props.onclick) {
|
||||||
|
span(class=icon_class)
|
||||||
|
span { (props.text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Prop)]
|
||||||
|
pub struct TableProps<'a, G: Html> {
|
||||||
|
pub headers: Vec<String>,
|
||||||
|
pub rows: Vec<Vec<String>>,
|
||||||
|
pub loading: bool,
|
||||||
|
|
||||||
|
#[builder(default)]
|
||||||
|
pub children: Children<'a, G>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn table<'a, G: Html>(props: TableProps<'a, G>) -> View<G> {
|
||||||
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
div(class="inline-block min-w-full py-2 align-middle") {
|
||||||
|
div(class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") {
|
||||||
|
table(class="min-w-full divide-y divide-gray-600") {
|
||||||
|
thead(class="bg-gray-800") {
|
||||||
|
tr {
|
||||||
|
(props.headers.iter().map(|header| view! { cx,
|
||||||
|
th(
|
||||||
|
scope="col",
|
||||||
|
class="px-3 py-3.5 text-left
|
||||||
|
text-sm font-semibold text-gray-400"
|
||||||
|
) {
|
||||||
|
(header)
|
||||||
|
}
|
||||||
|
}).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody(class="divide-y divide-gray-600 bg-gray-700") {
|
||||||
|
(props.rows.iter().map(|row| view! { cx,
|
||||||
|
tr {
|
||||||
|
{row.iter().map(|value| html! {
|
||||||
|
td(class="whitespace-nowrap px-3 py-4 text-sm text-gray-400"){
|
||||||
|
(value)
|
||||||
|
}
|
||||||
|
}).collect()}
|
||||||
|
}
|
||||||
|
}).collect())
|
||||||
|
(children)
|
||||||
|
(if props.loading == true { view! {
|
||||||
|
tr {
|
||||||
|
td(colspan={(props.headers.len() + 1).to_string()} class="py-3") {
|
||||||
|
div(class="grid place-items-center") {
|
||||||
|
"Loading ..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}} else { view! {} })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,448 +1,92 @@
|
||||||
use crate::components::*;
|
use crate::components::Block;
|
||||||
use std::collections::{HashMap, HashSet};
|
use lan_party_core::event::EventSpec;
|
||||||
|
use sycamore::prelude::*;
|
||||||
use lan_party_core::event::{
|
|
||||||
free_for_all_game::{FreeForAllGame, FreeForAllGameRanking, FreeForAllGameSpec},
|
|
||||||
team_game::{TeamGame, TeamGameSpec},
|
|
||||||
test::{Test, TestSpec},
|
|
||||||
*,
|
|
||||||
};
|
|
||||||
use paste::paste;
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
use yew::prelude::*;
|
|
||||||
|
|
||||||
use crate::{bind, bind_change, bind_value, clone, clone_cb};
|
|
||||||
|
|
||||||
macro_rules! view_fields {
|
|
||||||
($(($name:expr, $prop:expr)),* $(,)?) => {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
$(
|
|
||||||
<p>
|
|
||||||
{ $name }
|
|
||||||
{ $prop.view() }
|
|
||||||
</p>
|
|
||||||
)*
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! view_struct {
|
|
||||||
($struct:path: $title:expr => $(($name:expr, $prop:ident)),* $(,)?) => {
|
|
||||||
impl View for $struct {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<Block title={$title}>
|
|
||||||
{ view_fields!(
|
|
||||||
$(($name, self.$prop),)*
|
|
||||||
)}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($struct:path as $self:ident => $body:expr) => {
|
|
||||||
impl View for $struct {
|
|
||||||
fn view(&$self) -> Html {
|
|
||||||
$body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! view_enum_simple {
|
|
||||||
($enum:path: $($variant:ident),* $(,)?) => {
|
|
||||||
impl View for $enum {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
{match self {
|
|
||||||
$(
|
|
||||||
Self::$variant(i) => html! {
|
|
||||||
<>
|
|
||||||
{ i.view() }
|
|
||||||
</>
|
|
||||||
},
|
|
||||||
)*
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Properties, PartialEq)]
|
|
||||||
pub struct BlockProps {
|
|
||||||
title: String,
|
|
||||||
children: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(Block)]
|
|
||||||
pub fn block(props: &BlockProps) -> Html {
|
|
||||||
let open = use_state(|| false);
|
|
||||||
let mut class = classes!("overflow-hidden");
|
|
||||||
|
|
||||||
if !*open {
|
|
||||||
class.push("max-h-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<div class="px-3 py-3 rounded-lg border-solid border-gray-800 border-2 my-3">
|
|
||||||
<p class="cursor-pointer text-lg" onclick={clone_cb!(open => move |_| open.set(!*open))}>{ &props.title }</p>
|
|
||||||
<div {class}>
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
{for props.children.iter()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait View {
|
|
||||||
fn view(&self) -> Html;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for bool {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<input type="checkbox" value={self.to_string()} disabled={true} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ViewPlain: Into<Html> + std::fmt::Display {}
|
|
||||||
|
|
||||||
impl<T> View for T
|
|
||||||
where
|
|
||||||
T: ViewPlain,
|
|
||||||
{
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ViewPlain for i64 {}
|
|
||||||
impl ViewPlain for i32 {}
|
|
||||||
impl ViewPlain for isize {}
|
|
||||||
|
|
||||||
impl ViewPlain for u64 {}
|
|
||||||
impl ViewPlain for u32 {}
|
|
||||||
impl ViewPlain for usize {}
|
|
||||||
|
|
||||||
impl ViewPlain for f64 {}
|
|
||||||
impl ViewPlain for f32 {}
|
|
||||||
|
|
||||||
impl ViewPlain for String {}
|
|
||||||
impl<'a> ViewPlain for &'a str {}
|
|
||||||
|
|
||||||
macro_rules! view_iter {
|
|
||||||
($t:ident => $type:ty) => {
|
|
||||||
impl<$t: View> View for $type {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<Block title="List">
|
|
||||||
<ul>
|
|
||||||
{ self.iter().map(|x| html! { <li>{ x.view() }</li> }).collect::<Html>() }
|
|
||||||
</ul>
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
view_iter!(T => Vec<T>);
|
|
||||||
view_iter!(T => HashSet<T>);
|
|
||||||
view_iter!(T => &[T]);
|
|
||||||
|
|
||||||
impl<T: View> View for Option<T> {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
match self {
|
|
||||||
Some(content) => content.view(),
|
|
||||||
None => html! { "None" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: View, V: View> View for HashMap<K, V> {
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<Block title={"Map"}>
|
|
||||||
{self.iter().map(|(k, v)| {
|
|
||||||
html! { <p><span>{k.view()}</span>{ ": " }<span>{v.view()}</span></p> }
|
|
||||||
}).collect::<Html>()}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view_struct!(
|
|
||||||
lan_party_core::event::Event: "Event" =>
|
|
||||||
("Name: ", name),
|
|
||||||
("Description: ", description),
|
|
||||||
("Concluded: ", concluded),
|
|
||||||
("Event type: ", event_type),
|
|
||||||
);
|
|
||||||
|
|
||||||
view_enum_simple!(
|
|
||||||
lan_party_core::event::EventType: Test,
|
|
||||||
TeamGame,
|
|
||||||
FreeForAllGame
|
|
||||||
);
|
|
||||||
|
|
||||||
view_struct!(
|
|
||||||
lan_party_core::event::test::Test: "Test" =>
|
|
||||||
("Number of players: ", num_players)
|
|
||||||
);
|
|
||||||
|
|
||||||
view_struct!(FreeForAllGame as self =>
|
|
||||||
html! {
|
|
||||||
<Block title={"FreeForAllGame"}>
|
|
||||||
{ view_fields!(("Ranking: ", self.ranking)) }
|
|
||||||
{ view_fields!(
|
|
||||||
("Participants: ", self.spec.participants),
|
|
||||||
("Win rewards: ", self.spec.win_rewards),
|
|
||||||
("Lose rewards: ", self.spec.lose_rewards),
|
|
||||||
) }
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
view_enum_simple!(
|
|
||||||
lan_party_core::event::free_for_all_game::FreeForAllGameRanking: Ranking,
|
|
||||||
Scores
|
|
||||||
);
|
|
||||||
|
|
||||||
view_struct!(TeamGame as self =>
|
|
||||||
html! {
|
|
||||||
<Block title={"TeamGame"}>
|
|
||||||
{ view_fields!(("Teams: ", self.teams)) }
|
|
||||||
{ view_fields!(("Ranking: ", self.ffa_game.ranking)) }
|
|
||||||
{ view_fields!(
|
|
||||||
("Participants: ", self.ffa_game.spec.participants),
|
|
||||||
("Win rewards: ", self.ffa_game.spec.win_rewards),
|
|
||||||
("Lose rewards: ", self.ffa_game.spec.lose_rewards),
|
|
||||||
) }
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
macro_rules! edit_fields {
|
macro_rules! edit_fields {
|
||||||
($(($name:expr, $prop:expr)),* $(,)?) => {
|
($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => {
|
||||||
html! {
|
view! { $cx,
|
||||||
<>
|
|
||||||
$(
|
$(
|
||||||
<p>
|
p {
|
||||||
{ $name }
|
($name) ": " ($prop.edit($cx))
|
||||||
{ $prop.edit() }
|
}
|
||||||
</p>
|
|
||||||
)*
|
)*
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! link_fields {
|
macro_rules! link_fields {
|
||||||
($($field:ident),* $(,)? => $state:ident as $t:ident) => {
|
($cx:ident, $($field:ident),* $(,)? => $state:ident as $t:ident) => {
|
||||||
$(let $field = use_state(|| $state.$field.clone());)*
|
$(let $field = create_signal($cx, $state.get().$field.clone());)*
|
||||||
|
|
||||||
use_effect_with_deps(
|
create_effect($cx, || {
|
||||||
clone!($($field,)* $state =>
|
$state.set(Self {
|
||||||
move |_| {
|
$($field: $field.get().as_ref().clone(),)*
|
||||||
$state.set($t {
|
|
||||||
$($field: (*$field).clone(),)*
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|| { $(drop($field);)* drop($state); }
|
});
|
||||||
}
|
|
||||||
),
|
|
||||||
($($field.clone(),)*),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! link_variants {
|
|
||||||
($selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)? => $state:ident as $t:ident) => {
|
|
||||||
let $selected = use_state(|| 0 as usize);
|
|
||||||
$(let $var_name = if let $t::$variant(v) = &*$state {
|
|
||||||
use_state(|| v.clone())
|
|
||||||
} else {
|
|
||||||
use_state(|| <$var_type>::default())
|
|
||||||
};)*
|
|
||||||
|
|
||||||
use_effect_with_deps(
|
|
||||||
clone!($($var_name,)* $state, $selected =>
|
|
||||||
move |_| {
|
|
||||||
match *$selected {
|
|
||||||
$($index => $state.set($t::$variant((*$var_name).clone())),)*
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
|| { $(drop($var_name);)* drop($selected); drop($state); }
|
|
||||||
}
|
|
||||||
),
|
|
||||||
($($var_name.clone(),)* $selected.clone()),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! edit_struct {
|
macro_rules! edit_struct {
|
||||||
($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => {
|
($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => {
|
||||||
paste! {
|
impl<'a, G: Html> Edit<'a, G> for $struct {
|
||||||
#[function_component([<$struct Edit>])]
|
fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View<G> {
|
||||||
pub fn [<$struct:lower _edit>](props: &EditProps<$struct>) -> Html {
|
let state = props.state;
|
||||||
let state = props.state.clone();
|
link_fields!(cx, $($prop,)* => state as Self);
|
||||||
link_fields!($($prop,)* => state as $struct);
|
view! { cx,
|
||||||
html! {
|
Block(title=stringify!($struct).into()) {
|
||||||
<Block title={stringify!($struct)}>
|
(edit_fields!(cx, $(($name, $prop),)*))
|
||||||
{ edit_fields!($(($name, $prop)),*) }
|
|
||||||
</Block>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editable for $struct {
|
|
||||||
type Edit = [<$struct Edit>];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! edit_enum {
|
#[derive(Prop)]
|
||||||
($enum:ident => $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => {
|
pub struct EditProps<'a, T> {
|
||||||
paste! {
|
pub state: &'a Signal<T>,
|
||||||
#[function_component([<$enum Edit>])]
|
|
||||||
pub fn [<$enum:lower _edit>](props: &EditProps<$enum>) -> Html {
|
|
||||||
let state = props.state.clone();
|
|
||||||
|
|
||||||
link_variants!($selected =>
|
|
||||||
$($index: $var_name = $variant: $var_type,)*
|
|
||||||
=> state as $enum
|
|
||||||
);
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Block title={stringify!($enum)}>
|
|
||||||
<Select class="" bind={bind!($selected)}>
|
|
||||||
$(<option value={stringify!($index)}>{ stringify!($variant) }</option>)*
|
|
||||||
</Select>
|
|
||||||
{ match &*state {
|
|
||||||
$($enum::$variant(_) => $var_name.edit(),)*
|
|
||||||
}}
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for $enum {
|
|
||||||
type Edit = [<$enum Edit>];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Properties)]
|
impl<'a, T> From<&'a Signal<T>> for EditProps<'a, T> {
|
||||||
pub struct EditProps<T: PartialEq> {
|
fn from(state: &'a Signal<T>) -> Self {
|
||||||
pub state: UseStateHandle<T>,
|
EditProps { state }
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Edit {
|
|
||||||
fn edit(&self) -> Html;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Comp: Component<Properties = EditProps<Type>>, Type: Editable<Edit = Comp> + PartialEq> Edit
|
|
||||||
for UseStateHandle<Type>
|
|
||||||
{
|
|
||||||
fn edit(&self) -> Html {
|
|
||||||
html!(<Comp state={self.clone()} />)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Editable {
|
pub trait IntoEdit<'a, G: Html> {
|
||||||
type Edit;
|
fn edit(self, cx: Scope<'a>) -> View<G>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(InputEdit)]
|
impl<'a, G: Html, T: Edit<'a, G>> IntoEdit<'a, G> for &'a Signal<T>
|
||||||
pub fn input_edit<T>(props: &EditProps<T>) -> Html
|
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone + 'static,
|
EditProps<'a, T>: From<&'a Signal<T>>,
|
||||||
Binding<String>: From<Binding<T>>,
|
|
||||||
{
|
{
|
||||||
let string = props.state.clone();
|
fn edit(self, cx: Scope<'a>) -> View<G> {
|
||||||
|
T::edit(cx, self.into())
|
||||||
html! {
|
|
||||||
<TextInput
|
|
||||||
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
|
||||||
bind={bind!(string)}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Editable for T
|
pub trait Edit<'a, G: Html>: Sized {
|
||||||
where
|
fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View<G>;
|
||||||
T: PartialEq + Clone + 'static,
|
|
||||||
Binding<String>: From<Binding<T>>,
|
|
||||||
{
|
|
||||||
type Edit = InputEdit<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Stub)]
|
edit_struct!(EventSpec => ("Name", name));
|
||||||
pub fn stub<T: PartialEq>(props: &EditProps<T>) -> Html {
|
|
||||||
html! { "stub" }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for Vec<String> {
|
|
||||||
type Edit = Stub<Vec<String>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editable for HashMap<String, i64> {
|
|
||||||
type Edit = Stub<HashMap<String, i64>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
edit_struct!(EventSpec => ("Name: ", name), ("Description: ", description), ("Event type: ", event_type));
|
|
||||||
edit_struct!(TestSpec => ("Number of players: ", num_players));
|
|
||||||
edit_struct!(TeamGameSpec => );
|
|
||||||
edit_struct!(FreeForAllGameSpec => );
|
|
||||||
|
|
||||||
edit_enum!(EventTypeSpec => selected =>
|
|
||||||
0: test = Test: TestSpec,
|
|
||||||
1: team_game = TeamGame: TeamGameSpec,
|
|
||||||
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
|
||||||
);
|
|
||||||
|
|
||||||
edit_enum!(FreeForAllGameRanking => selected =>
|
|
||||||
0: ranking = Ranking: Vec<String>,
|
|
||||||
1: scores = Scores: HashMap<String, i64>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[function_component(EventTypeSpecEdit)]
|
impl<'a, G: Html> Edit<'a, G> for EventSpec {
|
||||||
pub fn event_type_spec_edit(props: &EditProps<EventTypeSpec>) -> Html {
|
fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View<G> {
|
||||||
let state = props.state.clone();
|
let state = props.state;
|
||||||
|
link_fields!(cx, name => state as Self);
|
||||||
link_variants!(selected =>
|
edit_fields!(cx, ("Name", name))
|
||||||
0: test = Test: TestSpec,
|
|
||||||
1: team_game = TeamGame: TeamGameSpec,
|
|
||||||
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
|
||||||
=> state as EventTypeSpec
|
|
||||||
);
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Block title={"EventTypeSpec"}>
|
|
||||||
<Select class="" bind={bind!(selected)}>
|
|
||||||
<option value="0">{ "Test" }</option>
|
|
||||||
<option value="1">{ "TeamGame" }</option>
|
|
||||||
<option value="2">{ "FreeForAllGame" }</option>
|
|
||||||
</Select>
|
|
||||||
{ match &*state {
|
|
||||||
EventTypeSpec::Test(_) => test.edit(),
|
|
||||||
EventTypeSpec::TeamGame(_) => team_game.edit(),
|
|
||||||
EventTypeSpec::FreeForAllGame(_) => free_for_all_game.edit(),
|
|
||||||
}}
|
|
||||||
</Block>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editable for EventTypeSpec {
|
|
||||||
type Edit = EventTypeSpecEdit;
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
impl<'a, G: Html> Edit<'a, G> for String {
|
||||||
|
fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View<G> {
|
||||||
|
view! { cx,
|
||||||
|
input(bind:value=props.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,120 +1,117 @@
|
||||||
mod button;
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
mod table;
|
|
||||||
|
|
||||||
pub use button::Button;
|
use sycamore::prelude::*;
|
||||||
pub use event::View;
|
use web_sys::Event;
|
||||||
pub use table::Table;
|
|
||||||
use yew::{function_component, html, Children, Properties};
|
|
||||||
|
|
||||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
#[derive(Prop)]
|
||||||
use web_sys::{Event, HtmlInputElement, HtmlSelectElement, InputEvent};
|
pub struct PageProps<'a, G: Html> {
|
||||||
use yew::prelude::*;
|
pub children: Children<'a, G>,
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct Binding<T> {
|
|
||||||
pub onchange: Callback<T>,
|
|
||||||
pub value: T,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Binding<T> {
|
#[component]
|
||||||
pub fn new(value: T, onchange: Callback<T>) -> Self {
|
pub fn Page<'a, G: Html>(cx: Scope<'a>, props: PageProps<'a, G>) -> View<G> {
|
||||||
Self { value, onchange }
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
div(class="max-w-7xl mx-auto") { (children) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Binding<i64>> for Binding<String> {
|
#[derive(Prop)]
|
||||||
fn from(val: Binding<i64>) -> Self {
|
pub struct Props<F: FnMut(Event)> {
|
||||||
Binding {
|
pub onclick: F,
|
||||||
onchange: val.onchange.reform(|s: String| s.parse().unwrap_or(0)),
|
#[builder(default)]
|
||||||
value: val.value.to_string(),
|
pub text: String,
|
||||||
|
#[builder(default)]
|
||||||
|
pub icon: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: Props<F>) -> View<G> {
|
||||||
|
let mut icon_class = String::from("mdi text-lg ");
|
||||||
|
|
||||||
|
if !props.icon.is_empty() {
|
||||||
|
icon_class.push_str(&props.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
button(class="bg-gray-700 hover:bg-gray-800 text-gray-400 font-bold py-2 px-2 rounded inline-flex items-center",on:click=props.onclick) {
|
||||||
|
span(class=icon_class)
|
||||||
|
span { (props.text) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Binding<usize>> for Binding<String> {
|
#[derive(Prop)]
|
||||||
fn from(val: Binding<usize>) -> Self {
|
pub struct TableProps<'a, G: Html> {
|
||||||
Binding {
|
pub headers: Vec<String>,
|
||||||
onchange: val.onchange.reform(|s: String| s.parse().unwrap_or(0)),
|
|
||||||
value: val.value.to_string(),
|
pub children: Children<'a, G>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
||||||
|
let children = props.children.call(cx);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
div(class="inline-block min-w-full py-2 align-middle") {
|
||||||
|
div(class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") {
|
||||||
|
table(class="min-w-full divide-y divide-gray-600") {
|
||||||
|
thead(class="bg-gray-800") {
|
||||||
|
tr {
|
||||||
|
(View::new_fragment(props.headers.iter().cloned().map(|header| view! { cx,
|
||||||
|
th(
|
||||||
|
scope="col",
|
||||||
|
class="px-3 py-3.5 text-left
|
||||||
|
text-sm font-semibold text-gray-400"
|
||||||
|
) {
|
||||||
|
(header)
|
||||||
|
}
|
||||||
|
}).collect()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody(class="divide-y divide-gray-600 bg-gray-700") {
|
||||||
|
(children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Properties)]
|
#[derive(Prop)]
|
||||||
pub struct InputProps {
|
pub struct BlockProps<'a, G: Html> {
|
||||||
pub bind: Binding<String>,
|
title: String,
|
||||||
pub class: Classes,
|
children: Children<'a, G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_value_from_input_event(e: InputEvent) -> String {
|
#[component]
|
||||||
let event: Event = e.dyn_into().unwrap_throw();
|
pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
|
||||||
let event_target = event.target().unwrap_throw();
|
let children = props.children.call(cx);
|
||||||
let target: HtmlInputElement = event_target.dyn_into().unwrap_throw();
|
let open = create_signal(cx, false);
|
||||||
target.value()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(TextInput)]
|
let class = create_memo(cx, || {
|
||||||
pub fn text_input(props: &InputProps) -> Html {
|
if *open.get() {
|
||||||
let InputProps {
|
"overflow-hidden"
|
||||||
class,
|
} else {
|
||||||
bind: Binding { onchange, value },
|
"overflow-hidden max-h-1"
|
||||||
} = props.clone();
|
}
|
||||||
|
|
||||||
let oninput = Callback::from(move |input_event: InputEvent| {
|
|
||||||
onchange.emit(get_value_from_input_event(input_event));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
view! { cx,
|
||||||
<input type="text" {class} {value} {oninput} />
|
div(class="px-3 py-3 rounded-lg border-solid border-gray-800 border-2 my-3") {
|
||||||
|
p(class="cursor-pointer text-lg",on:click=move |_| open.set(!*open.get())) { (props.title) }
|
||||||
|
div(class=class) {
|
||||||
|
br {}
|
||||||
|
div {
|
||||||
|
(children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Properties)]
|
/*
|
||||||
pub struct SelectProps {
|
|
||||||
pub bind: Binding<String>,
|
|
||||||
pub class: Classes,
|
|
||||||
pub children: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value_from_select_event(e: InputEvent) -> String {
|
|
||||||
let event: Event = e.dyn_into().unwrap_throw();
|
|
||||||
let event_target = event.target().unwrap_throw();
|
|
||||||
let target: HtmlSelectElement = event_target.dyn_into().unwrap_throw();
|
|
||||||
target.value()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(Select)]
|
|
||||||
pub fn select(props: &SelectProps) -> Html {
|
|
||||||
let SelectProps {
|
|
||||||
class,
|
|
||||||
children,
|
|
||||||
bind: Binding { onchange, value },
|
|
||||||
} = props.clone();
|
|
||||||
|
|
||||||
let oninput = Callback::from(move |event: InputEvent| {
|
|
||||||
web_sys::console::log_1(&JsValue::from("select"));
|
|
||||||
onchange.emit(get_value_from_select_event(event));
|
|
||||||
});
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<select {class} {value} {oninput}>
|
|
||||||
{for children.iter()}
|
|
||||||
</select>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq, Default)]
|
|
||||||
pub struct PageProps {
|
|
||||||
#[prop_or_default]
|
|
||||||
pub children: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(Page)]
|
|
||||||
pub fn page(props: &PageProps) -> Html {
|
|
||||||
html! { <div class="max-w-7xl mx-auto">{ for props.children.iter() }</div> }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(Loading)]
|
#[function_component(Loading)]
|
||||||
pub fn loading() -> Html {
|
pub fn loading() -> Html {
|
||||||
html! {
|
html! {
|
||||||
|
@ -129,3 +126,4 @@ pub fn loading() -> Html {
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -0,0 +1,448 @@
|
||||||
|
use crate::components::*;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use lan_party_core::event::{
|
||||||
|
free_for_all_game::{FreeForAllGame, FreeForAllGameRanking, FreeForAllGameSpec},
|
||||||
|
team_game::{TeamGame, TeamGameSpec},
|
||||||
|
test::{Test, TestSpec},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use paste::paste;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::{bind, bind_change, bind_value, clone, clone_cb};
|
||||||
|
|
||||||
|
macro_rules! view_fields {
|
||||||
|
($(($name:expr, $prop:expr)),* $(,)?) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
$(
|
||||||
|
<p>
|
||||||
|
{ $name }
|
||||||
|
{ $prop.view() }
|
||||||
|
</p>
|
||||||
|
)*
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! view_struct {
|
||||||
|
($struct:path: $title:expr => $(($name:expr, $prop:ident)),* $(,)?) => {
|
||||||
|
impl View for $struct {
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<Block title={$title}>
|
||||||
|
{ view_fields!(
|
||||||
|
$(($name, self.$prop),)*
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($struct:path as $self:ident => $body:expr) => {
|
||||||
|
impl View for $struct {
|
||||||
|
fn view(&$self) -> Html {
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! view_enum_simple {
|
||||||
|
($enum:path: $($variant:ident),* $(,)?) => {
|
||||||
|
impl View for $enum {
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
{match self {
|
||||||
|
$(
|
||||||
|
Self::$variant(i) => html! {
|
||||||
|
<>
|
||||||
|
{ i.view() }
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
)*
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Properties, PartialEq)]
|
||||||
|
pub struct BlockProps {
|
||||||
|
title: String,
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Block)]
|
||||||
|
pub fn block(props: &BlockProps) -> Html {
|
||||||
|
let open = use_state(|| false);
|
||||||
|
let mut class = classes!("overflow-hidden");
|
||||||
|
|
||||||
|
if !*open {
|
||||||
|
class.push("max-h-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<div class="px-3 py-3 rounded-lg border-solid border-gray-800 border-2 my-3">
|
||||||
|
<p class="cursor-pointer text-lg" onclick={clone_cb!(open => move |_| open.set(!*open))}>{ &props.title }</p>
|
||||||
|
<div {class}>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
{for props.children.iter()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait View {
|
||||||
|
fn view(&self) -> Html;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for bool {
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<input type="checkbox" value={self.to_string()} disabled={true} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ViewPlain: Into<Html> + std::fmt::Display {}
|
||||||
|
|
||||||
|
impl<T> View for T
|
||||||
|
where
|
||||||
|
T: ViewPlain,
|
||||||
|
{
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewPlain for i64 {}
|
||||||
|
impl ViewPlain for i32 {}
|
||||||
|
impl ViewPlain for isize {}
|
||||||
|
|
||||||
|
impl ViewPlain for u64 {}
|
||||||
|
impl ViewPlain for u32 {}
|
||||||
|
impl ViewPlain for usize {}
|
||||||
|
|
||||||
|
impl ViewPlain for f64 {}
|
||||||
|
impl ViewPlain for f32 {}
|
||||||
|
|
||||||
|
impl ViewPlain for String {}
|
||||||
|
impl<'a> ViewPlain for &'a str {}
|
||||||
|
|
||||||
|
macro_rules! view_iter {
|
||||||
|
($t:ident => $type:ty) => {
|
||||||
|
impl<$t: View> View for $type {
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<Block title="List">
|
||||||
|
<ul>
|
||||||
|
{ self.iter().map(|x| html! { <li>{ x.view() }</li> }).collect::<Html>() }
|
||||||
|
</ul>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
view_iter!(T => Vec<T>);
|
||||||
|
view_iter!(T => HashSet<T>);
|
||||||
|
view_iter!(T => &[T]);
|
||||||
|
|
||||||
|
impl<T: View> View for Option<T> {
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
match self {
|
||||||
|
Some(content) => content.view(),
|
||||||
|
None => html! { "None" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: View, V: View> View for HashMap<K, V> {
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<Block title={"Map"}>
|
||||||
|
{self.iter().map(|(k, v)| {
|
||||||
|
html! { <p><span>{k.view()}</span>{ ": " }<span>{v.view()}</span></p> }
|
||||||
|
}).collect::<Html>()}
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view_struct!(
|
||||||
|
lan_party_core::event::Event: "Event" =>
|
||||||
|
("Name: ", name),
|
||||||
|
("Description: ", description),
|
||||||
|
("Concluded: ", concluded),
|
||||||
|
("Event type: ", event_type),
|
||||||
|
);
|
||||||
|
|
||||||
|
view_enum_simple!(
|
||||||
|
lan_party_core::event::EventType: Test,
|
||||||
|
TeamGame,
|
||||||
|
FreeForAllGame
|
||||||
|
);
|
||||||
|
|
||||||
|
view_struct!(
|
||||||
|
lan_party_core::event::test::Test: "Test" =>
|
||||||
|
("Number of players: ", num_players)
|
||||||
|
);
|
||||||
|
|
||||||
|
view_struct!(FreeForAllGame as self =>
|
||||||
|
html! {
|
||||||
|
<Block title={"FreeForAllGame"}>
|
||||||
|
{ view_fields!(("Ranking: ", self.ranking)) }
|
||||||
|
{ view_fields!(
|
||||||
|
("Participants: ", self.spec.participants),
|
||||||
|
("Win rewards: ", self.spec.win_rewards),
|
||||||
|
("Lose rewards: ", self.spec.lose_rewards),
|
||||||
|
) }
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
view_enum_simple!(
|
||||||
|
lan_party_core::event::free_for_all_game::FreeForAllGameRanking: Ranking,
|
||||||
|
Scores
|
||||||
|
);
|
||||||
|
|
||||||
|
view_struct!(TeamGame as self =>
|
||||||
|
html! {
|
||||||
|
<Block title={"TeamGame"}>
|
||||||
|
{ view_fields!(("Teams: ", self.teams)) }
|
||||||
|
{ view_fields!(("Ranking: ", self.ffa_game.ranking)) }
|
||||||
|
{ view_fields!(
|
||||||
|
("Participants: ", self.ffa_game.spec.participants),
|
||||||
|
("Win rewards: ", self.ffa_game.spec.win_rewards),
|
||||||
|
("Lose rewards: ", self.ffa_game.spec.lose_rewards),
|
||||||
|
) }
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
macro_rules! edit_fields {
|
||||||
|
($(($name:expr, $prop:expr)),* $(,)?) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
$(
|
||||||
|
<p>
|
||||||
|
{ $name }
|
||||||
|
{ $prop.edit() }
|
||||||
|
</p>
|
||||||
|
)*
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! link_fields {
|
||||||
|
($($field:ident),* $(,)? => $state:ident as $t:ident) => {
|
||||||
|
$(let $field = use_state(|| $state.$field.clone());)*
|
||||||
|
|
||||||
|
use_effect_with_deps(
|
||||||
|
clone!($($field,)* $state =>
|
||||||
|
move |_| {
|
||||||
|
$state.set($t {
|
||||||
|
$($field: (*$field).clone(),)*
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|| { $(drop($field);)* drop($state); }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
($($field.clone(),)*),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! link_variants {
|
||||||
|
($selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)? => $state:ident as $t:ident) => {
|
||||||
|
let $selected = use_state(|| 0 as usize);
|
||||||
|
$(let $var_name = if let $t::$variant(v) = &*$state {
|
||||||
|
use_state(|| v.clone())
|
||||||
|
} else {
|
||||||
|
use_state(|| <$var_type>::default())
|
||||||
|
};)*
|
||||||
|
|
||||||
|
use_effect_with_deps(
|
||||||
|
clone!($($var_name,)* $state, $selected =>
|
||||||
|
move |_| {
|
||||||
|
match *$selected {
|
||||||
|
$($index => $state.set($t::$variant((*$var_name).clone())),)*
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
|| { $(drop($var_name);)* drop($selected); drop($state); }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
($($var_name.clone(),)* $selected.clone()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! edit_struct {
|
||||||
|
($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => {
|
||||||
|
paste! {
|
||||||
|
#[function_component([<$struct Edit>])]
|
||||||
|
pub fn [<$struct:lower _edit>](props: &EditProps<$struct>) -> Html {
|
||||||
|
let state = props.state.clone();
|
||||||
|
link_fields!($($prop,)* => state as $struct);
|
||||||
|
html! {
|
||||||
|
<Block title={stringify!($struct)}>
|
||||||
|
{ edit_fields!($(($name, $prop)),*) }
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editable for $struct {
|
||||||
|
type Edit = [<$struct Edit>];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! edit_enum {
|
||||||
|
($enum:ident => $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => {
|
||||||
|
paste! {
|
||||||
|
#[function_component([<$enum Edit>])]
|
||||||
|
pub fn [<$enum:lower _edit>](props: &EditProps<$enum>) -> Html {
|
||||||
|
let state = props.state.clone();
|
||||||
|
|
||||||
|
link_variants!($selected =>
|
||||||
|
$($index: $var_name = $variant: $var_type,)*
|
||||||
|
=> state as $enum
|
||||||
|
);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Block title={stringify!($enum)}>
|
||||||
|
<Select class="" bind={bind!($selected)}>
|
||||||
|
$(<option value={stringify!($index)}>{ stringify!($variant) }</option>)*
|
||||||
|
</Select>
|
||||||
|
{ match &*state {
|
||||||
|
$($enum::$variant(_) => $var_name.edit(),)*
|
||||||
|
}}
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editable for $enum {
|
||||||
|
type Edit = [<$enum Edit>];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties)]
|
||||||
|
pub struct EditProps<T: PartialEq> {
|
||||||
|
pub state: UseStateHandle<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Edit {
|
||||||
|
fn edit(&self) -> Html;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Comp: Component<Properties = EditProps<Type>>, Type: Editable<Edit = Comp> + PartialEq> Edit
|
||||||
|
for UseStateHandle<Type>
|
||||||
|
{
|
||||||
|
fn edit(&self) -> Html {
|
||||||
|
html!(<Comp state={self.clone()} />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Editable {
|
||||||
|
type Edit: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(InputEdit)]
|
||||||
|
pub fn input_edit<T>(props: &EditProps<T>) -> Html
|
||||||
|
where
|
||||||
|
T: PartialEq + Clone + 'static,
|
||||||
|
Binding<String>: From<Binding<T>>,
|
||||||
|
{
|
||||||
|
let string = props.state.clone();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<TextInput
|
||||||
|
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
||||||
|
bind={bind!(string)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Editable for T
|
||||||
|
where
|
||||||
|
T: PartialEq + Clone + 'static,
|
||||||
|
Binding<String>: From<Binding<T>>,
|
||||||
|
{
|
||||||
|
type Edit = InputEdit<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Stub)]
|
||||||
|
pub fn stub<T: PartialEq>(props: &EditProps<T>) -> Html {
|
||||||
|
html! { "stub" }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editable for Vec<String> {
|
||||||
|
type Edit = Stub<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editable for HashMap<String, i64> {
|
||||||
|
type Edit = Stub<HashMap<String, i64>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_struct!(EventSpec => ("Name: ", name), ("Description: ", description), ("Event type: ", event_type));
|
||||||
|
edit_struct!(TestSpec => ("Number of players: ", num_players));
|
||||||
|
edit_struct!(TeamGameSpec => );
|
||||||
|
edit_struct!(FreeForAllGameSpec => );
|
||||||
|
|
||||||
|
edit_enum!(EventTypeSpec => selected =>
|
||||||
|
0: test = Test: TestSpec,
|
||||||
|
1: team_game = TeamGame: TeamGameSpec,
|
||||||
|
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
||||||
|
);
|
||||||
|
|
||||||
|
edit_enum!(FreeForAllGameRanking => selected =>
|
||||||
|
0: ranking = Ranking: Vec<String>,
|
||||||
|
1: scores = Scores: HashMap<String, i64>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[function_component(EventTypeSpecEdit)]
|
||||||
|
pub fn event_type_spec_edit(props: &EditProps<EventTypeSpec>) -> Html {
|
||||||
|
let state = props.state.clone();
|
||||||
|
|
||||||
|
link_variants!(selected =>
|
||||||
|
0: test = Test: TestSpec,
|
||||||
|
1: team_game = TeamGame: TeamGameSpec,
|
||||||
|
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
||||||
|
=> state as EventTypeSpec
|
||||||
|
);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Block title={"EventTypeSpec"}>
|
||||||
|
<Select class="" bind={bind!(selected)}>
|
||||||
|
<option value="0">{ "Test" }</option>
|
||||||
|
<option value="1">{ "TeamGame" }</option>
|
||||||
|
<option value="2">{ "FreeForAllGame" }</option>
|
||||||
|
</Select>
|
||||||
|
{ match &*state {
|
||||||
|
EventTypeSpec::Test(_) => test.edit(),
|
||||||
|
EventTypeSpec::TeamGame(_) => team_game.edit(),
|
||||||
|
EventTypeSpec::FreeForAllGame(_) => free_for_all_game.edit(),
|
||||||
|
}}
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editable for EventTypeSpec {
|
||||||
|
type Edit = EventTypeSpecEdit;
|
||||||
|
}
|
||||||
|
*/
|
186
web/src/main.rs
186
web/src/main.rs
|
@ -3,131 +3,103 @@ mod pages;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use pages::{EventsPage, UsersPage};
|
use pages::{EventsPage, UsersPage};
|
||||||
use yew::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use sycamore_router::{HistoryIntegration, Route, Router};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Routable, PartialEq)]
|
#[derive(Route)]
|
||||||
enum Route {
|
enum AppRoutes {
|
||||||
#[at("/")]
|
#[to("/")]
|
||||||
Home,
|
Home,
|
||||||
#[at("/users")]
|
#[to("/users")]
|
||||||
Users,
|
Users,
|
||||||
#[at("/events")]
|
#[to("/events")]
|
||||||
Events,
|
Events,
|
||||||
}
|
#[not_found]
|
||||||
|
NotFound,
|
||||||
struct Model {
|
|
||||||
pages: Vec<Page>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Model {
|
|
||||||
type Message = ();
|
|
||||||
type Properties = ();
|
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
pages: vec![
|
|
||||||
Page {
|
|
||||||
target: Route::Home,
|
|
||||||
name: "Home".into(),
|
|
||||||
},
|
|
||||||
Page {
|
|
||||||
target: Route::Users,
|
|
||||||
name: "Users".into(),
|
|
||||||
},
|
|
||||||
Page {
|
|
||||||
target: Route::Events,
|
|
||||||
name: "Events".into(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
|
||||||
html! {
|
|
||||||
<BrowserRouter>
|
|
||||||
<Navbar pages={self.pages.clone()} />
|
|
||||||
<Switch<Route> render={Switch::render(switch)} />
|
|
||||||
</BrowserRouter>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn switch(routes: &Route) -> Html {
|
|
||||||
match routes {
|
|
||||||
Route::Home => html! { <h1>{ "Home" }</h1> },
|
|
||||||
Route::Users => html! {
|
|
||||||
<UsersPage />
|
|
||||||
},
|
|
||||||
Route::Events => html! { <EventsPage /> },
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
struct Page {
|
pub struct Page {
|
||||||
name: String,
|
name: String,
|
||||||
target: Route,
|
target: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq, Default)]
|
#[derive(Prop)]
|
||||||
struct NavbarProps {
|
pub struct NavbarProps<'a, G: Html> {
|
||||||
#[prop_or_default]
|
children: Children<'a, G>,
|
||||||
pub children: Children,
|
pages: Vec<Page>,
|
||||||
|
|
||||||
pub pages: Vec<Page>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Navbar)]
|
#[component]
|
||||||
fn navbar(props: &NavbarProps) -> Html {
|
pub fn Navbar<'a, G: Html>(cx: Scope<'a>, props: NavbarProps<'a, G>) -> View<G> {
|
||||||
let active_target = use_state(|| 0);
|
let children = props.children.call(cx);
|
||||||
|
view! { cx,
|
||||||
let history = use_history().unwrap();
|
nav(class="bg-gray-800") {
|
||||||
|
div(class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8") {
|
||||||
let onclick = |i: usize| {
|
div(class="relative flex h-16 items-center justify-between") {
|
||||||
let active_target = active_target.clone();
|
div(class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start") {
|
||||||
let history = history.clone();
|
div(class="hidden sm:ml-6 sm:block") {
|
||||||
let route = props.pages[i].target.clone();
|
div(class="flex space-x-4") {
|
||||||
|
(View::new_fragment(props.pages.iter().cloned().enumerate().map(|(_i, page)| {
|
||||||
Callback::from(move |_| {
|
view! { cx, a(href=page.target) { (page.name) } }
|
||||||
history.push(route.clone());
|
}).collect()))
|
||||||
active_target.set(i)
|
}
|
||||||
})
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
html! {
|
|
||||||
<nav class="bg-gray-800">
|
|
||||||
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
|
|
||||||
<div class="relative flex h-16 items-center justify-between">
|
|
||||||
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
|
|
||||||
<div class="hidden sm:ml-6 sm:block">
|
|
||||||
<div class="flex space-x-4">
|
|
||||||
{
|
|
||||||
props.pages.iter().enumerate().map(|(i, page)| {
|
|
||||||
let mut link_class =
|
|
||||||
Classes::from(&["text-sm", "font-medium", "rounded-md", "px-3", "py-2"] as &[&'static str]);
|
|
||||||
|
|
||||||
if *active_target == i {
|
|
||||||
link_class.push(&["bg-gray-900", "text-gray-400"] as &[&'static str])
|
|
||||||
} else {
|
|
||||||
link_class.push(&["hover:bg-gray-700", "text-gray-400"] as &[&'static str])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html! {
|
(children)
|
||||||
<a role={"button"} onclick={onclick(i)} class={link_class}>{&page.name}</a>
|
|
||||||
}
|
}
|
||||||
}).collect::<Html>()
|
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ for props.children.iter() }
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
yew::start_app::<Model>();
|
console_log::init_with_level(log::Level::Debug).unwrap();
|
||||||
|
|
||||||
|
let pages = vec![
|
||||||
|
Page {
|
||||||
|
target: "/".into(),
|
||||||
|
name: "Home".into(),
|
||||||
|
},
|
||||||
|
Page {
|
||||||
|
target: "/users".into(),
|
||||||
|
name: "Users".into(),
|
||||||
|
},
|
||||||
|
Page {
|
||||||
|
target: "/events".into(),
|
||||||
|
name: "Events".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
sycamore::render(move |cx| {
|
||||||
|
view! { cx,
|
||||||
|
Router(
|
||||||
|
integration=HistoryIntegration::new(),
|
||||||
|
view=|cx, route: &ReadSignal<AppRoutes>| {
|
||||||
|
view! { cx,
|
||||||
|
Navbar(pages=pages) {
|
||||||
|
|
||||||
|
}
|
||||||
|
div(class="app") {
|
||||||
|
(match route.get().as_ref() {
|
||||||
|
AppRoutes::Home => view! { cx,
|
||||||
|
"This is the home page"
|
||||||
|
},
|
||||||
|
AppRoutes::Users => view! { cx,
|
||||||
|
UsersPage
|
||||||
|
},
|
||||||
|
AppRoutes::Events => view! { cx,
|
||||||
|
EventsPage
|
||||||
|
},
|
||||||
|
AppRoutes::NotFound => view! { cx,
|
||||||
|
"404 Not Found"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,22 @@
|
||||||
use crate::{
|
|
||||||
components::{
|
|
||||||
event::{Edit, EventSpecEdit},
|
|
||||||
Button, Page, View,
|
|
||||||
},
|
|
||||||
init,
|
|
||||||
};
|
|
||||||
use lan_party_core::event::EventSpec;
|
use lan_party_core::event::EventSpec;
|
||||||
use wasm_bindgen::JsValue;
|
use log::debug;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use sycamore::prelude::*;
|
||||||
use yew::prelude::*;
|
|
||||||
use yew_hooks::*;
|
|
||||||
|
|
||||||
use crate::{clone, clone_cb, util::api_request};
|
use crate::components::{
|
||||||
|
event::{Edit, EditProps},
|
||||||
|
Block, Button, Page,
|
||||||
|
};
|
||||||
|
|
||||||
#[function_component(EventsPage)]
|
#[component]
|
||||||
pub fn events_page() -> Html {
|
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
let events = use_state(|| Vec::new());
|
let event_spec = create_signal(cx, EventSpec::default());
|
||||||
|
|
||||||
init!(events => {
|
view! { cx,
|
||||||
events.set(api_request::<_, Vec<lan_party_core::event::Event>>("GET", "/event", Option::<()>::None)
|
Page {
|
||||||
.await
|
Block(title="Create new event".into()) {
|
||||||
.map(|inner| inner.unwrap())
|
EventSpec::edit(EditProps { state: event_spec })
|
||||||
.unwrap())
|
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_spec.get()))
|
||||||
});
|
}
|
||||||
|
}
|
||||||
//let edit_event = use_state(|| EventSpecEditHandle::to_edit(EventSpec::default()));
|
|
||||||
let event_spec = use_state(|| EventSpec::default());
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Page>
|
|
||||||
{ events.view() }
|
|
||||||
|
|
||||||
{ event_spec.edit() }
|
|
||||||
|
|
||||||
<Button text="Create" onclick={clone_cb!(event_spec => move |_| web_sys::console::log_1(&JsValue::from_serde(&*event_spec).unwrap()))} />
|
|
||||||
</Page>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::{
|
||||||
|
components::{
|
||||||
|
event::{Edit, EventSpecEdit},
|
||||||
|
Button, Page, View,
|
||||||
|
},
|
||||||
|
init,
|
||||||
|
};
|
||||||
|
use lan_party_core::event::EventSpec;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_hooks::*;
|
||||||
|
|
||||||
|
use crate::{clone, clone_cb, util::api_request};
|
||||||
|
|
||||||
|
#[function_component(EventsPage)]
|
||||||
|
pub fn events_page() -> Html {
|
||||||
|
let events = use_state(|| Vec::new());
|
||||||
|
|
||||||
|
init!(events => {
|
||||||
|
events.set(api_request::<_, Vec<lan_party_core::event::Event>>(reqwasm::http::Method::GET, "/event", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.map(|inner| inner.unwrap())
|
||||||
|
.unwrap())
|
||||||
|
});
|
||||||
|
|
||||||
|
//let edit_event = use_state(|| EventSpecEditHandle::to_edit(EventSpec::default()));
|
||||||
|
let event_spec = use_state(|| EventSpec::default());
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Page>
|
||||||
|
{ events.view() }
|
||||||
|
|
||||||
|
{ event_spec.edit() }
|
||||||
|
|
||||||
|
<Button text="Create" onclick={clone_cb!(event_spec => move |_| web_sys::console::log_1(&JsValue::from_serde(&*event_spec).unwrap()))} />
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,109 +1,153 @@
|
||||||
use crate::{
|
use crate::components::{Button, Page, Table};
|
||||||
bind, bind_change, bind_value, clone, clone_cb, clone_cb_spawn,
|
|
||||||
components::{Binding, Button, Loading, Page, Table, TextInput},
|
|
||||||
init,
|
|
||||||
util::api_request,
|
|
||||||
};
|
|
||||||
use lan_party_core::user::User;
|
use lan_party_core::user::User;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use log::debug;
|
||||||
use yew::prelude::*;
|
use reqwasm::http::Method;
|
||||||
use yew_hooks::*;
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
use web_sys::Event;
|
||||||
|
|
||||||
#[function_component(UsersPage)]
|
use crate::util::api_request;
|
||||||
pub fn users_page() -> Html {
|
|
||||||
|
#[component]
|
||||||
|
pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
|
let users = create_signal(cx, Vec::<User>::new());
|
||||||
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
||||||
|
|
||||||
let new_username = use_state(|| String::new());
|
let score_edit = create_signal(cx, Option::<String>::None);
|
||||||
let score_edit: UseStateHandle<Option<usize>> = use_state(|| Option::None);
|
let new_score = create_signal(cx, String::new());
|
||||||
let current_score = use_state(|| String::new());
|
let new_username = create_signal(cx, String::new());
|
||||||
let users = use_state(|| Vec::new());
|
|
||||||
|
|
||||||
init!(users => {
|
spawn_local_scoped(cx, async move {
|
||||||
users.set(api_request::<_, Vec<User>>("GET", "/user", Option::<()>::None)
|
users.set(
|
||||||
|
api_request::<_, Vec<User>>(Method::GET, "/user", Option::<()>::None)
|
||||||
.await
|
.await
|
||||||
.map(|inner| inner.unwrap())
|
.map(|inner| inner.unwrap())
|
||||||
.unwrap());
|
.unwrap(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let oncheck = clone_cb_spawn!(score_edit, current_score, users => {
|
let ondelete = move |name: String| {
|
||||||
if let (Some(score_edit), Ok(score)) = (*score_edit, current_score.parse()) {
|
move |event: Event| {
|
||||||
let user: &User = &users[score_edit];
|
let name = name.clone();
|
||||||
api_request::<_, ()>("POST", &format!("/user/{}/score", user.name), Some(score))
|
spawn_local_scoped(cx, async move {
|
||||||
|
debug!("Delete {:#?}", event);
|
||||||
|
let users_ref = users.get();
|
||||||
|
let user: &User = users_ref.iter().find(|user| user.name == name).unwrap();
|
||||||
|
api_request::<_, ()>(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("/user/{}", user.name),
|
||||||
|
Option::<()>::None,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut cloned = (*users).clone();
|
let cloned = (*users_ref)
|
||||||
cloned[score_edit].score = score;
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter(|u| u.name != user.name)
|
||||||
|
.collect();
|
||||||
users.set(cloned);
|
users.set(cloned);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let oncheck = move |_| {
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
if let (Some(score_edit), Ok(score)) =
|
||||||
|
(score_edit.get().as_ref(), new_score.get().parse())
|
||||||
|
{
|
||||||
|
let score: i64 = score;
|
||||||
|
let users_ref = users.get();
|
||||||
|
let user: &User = users_ref
|
||||||
|
.iter()
|
||||||
|
.find(|user| &user.name == score_edit)
|
||||||
|
.unwrap();
|
||||||
|
api_request::<_, ()>(
|
||||||
|
Method::POST,
|
||||||
|
&format!("/user/{}/score", user.name),
|
||||||
|
Some(score),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let cloned = (*users_ref).clone();
|
||||||
|
let new_users: Vec<_> = cloned
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut user| {
|
||||||
|
if &user.name == score_edit {
|
||||||
|
user.score = score
|
||||||
|
}
|
||||||
|
user
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
users.set(new_users);
|
||||||
}
|
}
|
||||||
score_edit.set(None);
|
score_edit.set(None);
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let onedit = clone_cb!(current_score, score_edit, users => i => move |_| {
|
let onadd = move |_| {
|
||||||
let user: &User = &users[i];
|
spawn_local_scoped(cx, async move {
|
||||||
current_score.set(user.score.to_string());
|
let user = api_request::<String, User>(
|
||||||
score_edit.set(Some(i));
|
Method::POST,
|
||||||
});
|
"/user",
|
||||||
|
Some((*new_username).get().as_ref().clone()),
|
||||||
let ondelete = clone_cb_spawn!(users => i => {
|
)
|
||||||
let user: &User = &users[i];
|
.await
|
||||||
api_request::<_, ()>("DELETE", &format!("/user/{}", user.name), Option::<()>::None).await.unwrap();
|
.unwrap();
|
||||||
let cloned = users.iter().cloned().filter(|u| u.name != user.name).collect();
|
let mut cloned = (*users).get().as_ref().clone();
|
||||||
users.set(cloned);
|
|
||||||
});
|
|
||||||
|
|
||||||
let onadd = clone_cb_spawn!(new_username, users => {
|
|
||||||
let user = api_request::<String, User>("POST", "/user", Some((*new_username).clone())).await.unwrap();
|
|
||||||
let mut cloned = (*users).clone();
|
|
||||||
cloned.push(user.unwrap());
|
cloned.push(user.unwrap());
|
||||||
users.set(cloned);
|
users.set(cloned);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
html! {
|
view! { cx,
|
||||||
<Page>
|
Page {
|
||||||
<Table headers={headers.clone()} loading=false rows={vec![]}>
|
Table(headers=headers) {
|
||||||
{users.iter().enumerate().map(move |(i, user)| html! {
|
Keyed(
|
||||||
<tr>
|
iterable=users,
|
||||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400">{&user.name}</td>
|
view=move |cx, user| {
|
||||||
<td class="whitespace-nowrap px-3 text-sm text-slate-400">
|
let user = create_ref(cx, user);
|
||||||
{if Some(i) == *score_edit.clone() { html! {
|
view! { cx,
|
||||||
<>
|
tr {
|
||||||
<span class="inline-block">
|
td(class="whatespace-nowrap px-3 py-4 text-sm") { (user.name) }
|
||||||
<TextInput
|
td(class="whatespace-nowrap px-3 text-sm") {
|
||||||
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-20 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
(if Some(&user.name) == (*score_edit.get()).as_ref() { view! { cx,
|
||||||
bind={bind!(current_score)}
|
span(class="inline-block") {
|
||||||
/>
|
input(bind:value=new_score, class="mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-20 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500")
|
||||||
</span>
|
}
|
||||||
<Button icon={"mdi-check"} onclick={oncheck.clone()} />
|
Button(icon="mdi-check".into(), onclick=oncheck)
|
||||||
</>
|
}} else { view! { cx,
|
||||||
}} else { html! {
|
span(class="my-3,w-20") {
|
||||||
<>
|
(user.score)
|
||||||
<span class="my-3 w-20">
|
}
|
||||||
{user.score}
|
Button(
|
||||||
</span>
|
icon="mdi-pencil".into(),
|
||||||
<Button icon={"mdi-pencil"} onclick={onedit.clone()(i)} />
|
onclick=move |_| {
|
||||||
</>
|
score_edit.set(Some(user.name.clone()));
|
||||||
}}}
|
new_score.set(user.score.to_string());
|
||||||
</td>
|
}
|
||||||
<td class="whitespace-nowrap py-4 text-sm text-slate-400">
|
)
|
||||||
<Button icon={"mdi-delete"} onclick={ondelete.clone()(i)} />
|
}})
|
||||||
</td>
|
}
|
||||||
</tr>
|
td(class="whatespace-nowrap px-3 py-4 text-sm") {
|
||||||
}).collect::<Html>()}
|
Button(icon="mdi-delete".into(),onclick=ondelete(user.name.clone()))
|
||||||
<tr>
|
}
|
||||||
<td class="whitespace-nowrap px-3 text-sm text-slate-400">
|
}
|
||||||
<span class="inline-block">
|
}
|
||||||
<TextInput
|
},
|
||||||
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
key=|user| (user.name.clone(), user.score.clone()),
|
||||||
bind={bind!(new_username)}
|
)
|
||||||
/>
|
tr {
|
||||||
</span>
|
td(class="whatespace-nowrap px-3 text-sm") {
|
||||||
</td>
|
span(class="inline-block") {
|
||||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400"></td>
|
input(bind:value=new_username, class="mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500")
|
||||||
<td class="whitespace-nowrap py-4 text-sm text-slate-400">
|
}
|
||||||
<Button icon={"mdi-plus"} onclick={onadd} />
|
}
|
||||||
</td>
|
td(class="whatespace-nowrap px-3 py-4 text-sm") {}
|
||||||
</tr>
|
td(class="whatespace-nowrap py-4 px-3 text-sm") {
|
||||||
</Table>
|
Button(icon="mdi-plus".into(),onclick=onadd)
|
||||||
</Page>
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
use crate::{
|
||||||
|
bind, bind_change, bind_value, clone, clone_cb, clone_cb_spawn,
|
||||||
|
components::{Binding, Button, Loading, Page, Table, TextInput},
|
||||||
|
init,
|
||||||
|
util::api_request,
|
||||||
|
};
|
||||||
|
use lan_party_core::user::User;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_hooks::*;
|
||||||
|
|
||||||
|
#[function_component(UsersPage)]
|
||||||
|
pub fn users_page() -> Html {
|
||||||
|
let headers = vec!["Username".into(), "Score".into(), "".into()];
|
||||||
|
|
||||||
|
let new_username = use_state(|| String::new());
|
||||||
|
let score_edit: UseStateHandle<Option<usize>> = use_state(|| Option::None);
|
||||||
|
let current_score = use_state(|| String::new());
|
||||||
|
let users = use_state(|| Vec::new());
|
||||||
|
|
||||||
|
init!(users => {
|
||||||
|
users.set(api_request::<_, Vec<User>>(reqwasm::http::Method::GET, "/user", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.map(|inner| inner.unwrap())
|
||||||
|
.unwrap());
|
||||||
|
});
|
||||||
|
|
||||||
|
let oncheck = clone_cb_spawn!(score_edit, current_score, users => {
|
||||||
|
if let (Some(score_edit), Ok(score)) = (*score_edit, current_score.parse()) {
|
||||||
|
let user: &User = &users[score_edit];
|
||||||
|
api_request::<_, ()>(reqwasm::http::Method::POST, &format!("/user/{}/score", user.name), Some(score))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut cloned = (*users).clone();
|
||||||
|
cloned[score_edit].score = score;
|
||||||
|
users.set(cloned);
|
||||||
|
}
|
||||||
|
score_edit.set(None);
|
||||||
|
});
|
||||||
|
|
||||||
|
let onedit = clone_cb!(current_score, score_edit, users => i => move |_| {
|
||||||
|
let user: &User = &users[i];
|
||||||
|
current_score.set(user.score.to_string());
|
||||||
|
score_edit.set(Some(i));
|
||||||
|
});
|
||||||
|
|
||||||
|
let ondelete = clone_cb_spawn!(users => i => {
|
||||||
|
let user: &User = &users[i];
|
||||||
|
api_request::<_, ()>(reqwasm::http::Method::DELETE, &format!("/user/{}", user.name), Option::<()>::None).await.unwrap();
|
||||||
|
let cloned = users.iter().cloned().filter(|u| u.name != user.name).collect();
|
||||||
|
users.set(cloned);
|
||||||
|
});
|
||||||
|
|
||||||
|
let onadd = clone_cb_spawn!(new_username, users => {
|
||||||
|
let user = api_request::<String, User>(reqwasm::http::Method::POST, "/user", Some((*new_username).clone())).await.unwrap();
|
||||||
|
let mut cloned = (*users).clone();
|
||||||
|
cloned.push(user.unwrap());
|
||||||
|
users.set(cloned);
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Page>
|
||||||
|
<Table headers={headers.clone()} loading=false rows={vec![]}>
|
||||||
|
{users.iter().enumerate().map(move |(i, user)| html! {
|
||||||
|
<tr>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400">{&user.name}</td>
|
||||||
|
<td class="whitespace-nowrap px-3 text-sm text-slate-400">
|
||||||
|
{if Some(i) == *score_edit.clone() { html! {
|
||||||
|
<>
|
||||||
|
<span class="inline-block">
|
||||||
|
<TextInput
|
||||||
|
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-20 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
||||||
|
bind={bind!(current_score)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<Button icon={"mdi-check"} onclick={oncheck.clone()} />
|
||||||
|
</>
|
||||||
|
}} else { html! {
|
||||||
|
<>
|
||||||
|
<span class="my-3 w-20">
|
||||||
|
{user.score}
|
||||||
|
</span>
|
||||||
|
<Button icon={"mdi-pencil"} onclick={onedit.clone()(i)} />
|
||||||
|
</>
|
||||||
|
}}}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap py-4 text-sm text-slate-400">
|
||||||
|
<Button icon={"mdi-delete"} onclick={ondelete.clone()(i)} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}).collect::<Html>()}
|
||||||
|
<tr>
|
||||||
|
<td class="whitespace-nowrap px-3 text-sm text-slate-400">
|
||||||
|
<span class="inline-block">
|
||||||
|
<TextInput
|
||||||
|
class={"mx-2 appearance-none block bg-gray-700 text-slate-400 border border-gray-600 rounded py-2 px-2 w-50 leading-tight focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"}
|
||||||
|
bind={bind!(new_username)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400"></td>
|
||||||
|
<td class="whitespace-nowrap py-4 text-sm text-slate-400">
|
||||||
|
<Button icon={"mdi-plus"} onclick={onadd} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Table>
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,78 +1,43 @@
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use reqwasm::http::{Method, Request};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use wasm_bindgen::{prelude::*, JsCast};
|
use wasm_bindgen::{prelude::*, JsCast};
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
|
use web_sys::{Headers, RequestInit, RequestMode, Response};
|
||||||
use yew::html::IntoPropValue;
|
use yew::html::IntoPropValue;
|
||||||
|
|
||||||
#[derive(Error, Debug, Clone)]
|
|
||||||
pub enum JsError {
|
|
||||||
#[error("javascript error: {0}")]
|
|
||||||
JsError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JsValue> for JsError {
|
|
||||||
fn from(value: JsValue) -> Self {
|
|
||||||
Self::JsError(
|
|
||||||
js_sys::JSON::stringify(&value)
|
|
||||||
.unwrap()
|
|
||||||
.as_string()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn api_request<B: Serialize, R: for<'a> Deserialize<'a>>(
|
pub async fn api_request<B: Serialize, R: for<'a> Deserialize<'a>>(
|
||||||
method: &str,
|
method: Method,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: Option<B>,
|
body: Option<B>,
|
||||||
) -> Result<Option<R>, anyhow::Error> {
|
) -> Result<Option<R>, anyhow::Error> {
|
||||||
let api_key = "7de10bf6-278d-11ed-ad60-a8a15919d1b3";
|
let api_key = "7de10bf6-278d-11ed-ad60-a8a15919d1b3";
|
||||||
|
|
||||||
let mut req_opts = RequestInit::new();
|
let mut req = Request::new(&format!("http://localhost:8000/api/{}", endpoint))
|
||||||
req_opts.method(method);
|
.method(method)
|
||||||
|
.header("X-API-Key", api_key)
|
||||||
let headers = Headers::new().map_err(JsError::from)?;
|
.mode(RequestMode::Cors);
|
||||||
headers
|
|
||||||
.append("X-API-Key", api_key)
|
|
||||||
.map_err(JsError::from)?;
|
|
||||||
req_opts.headers(&headers);
|
|
||||||
|
|
||||||
if let Some(body) = body {
|
if let Some(body) = body {
|
||||||
let value = JsValue::from_serde(&body)?;
|
let value = JsValue::from_serde(&body)?;
|
||||||
req_opts.body(
|
req = req.body(
|
||||||
js_sys::JSON::stringify(&value)
|
js_sys::JSON::stringify(&value)
|
||||||
.ok()
|
.ok()
|
||||||
.map(JsValue::from)
|
.map(JsValue::from)
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
web_sys::console::log_1(&JsValue::from("here"));
|
|
||||||
|
|
||||||
let request = Request::new_with_str_and_init(
|
let res = req.send().await?;
|
||||||
&format!("http://localhost:8000/api/{}", endpoint),
|
|
||||||
&req_opts,
|
|
||||||
)
|
|
||||||
.map_err(JsError::from)?;
|
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
|
||||||
.await
|
|
||||||
.map_err(JsError::from)?;
|
|
||||||
let resp: Response = resp_value.dyn_into().unwrap();
|
|
||||||
|
|
||||||
let json = resp.json().map_err(JsError::from)?;
|
|
||||||
|
|
||||||
if let Ok(data) = JsFuture::from(json).await.map_err(JsError::from) {
|
|
||||||
if let Ok(data) = data.into_serde() {
|
|
||||||
return Ok(Some(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if let Ok(json) = res.json().await {
|
||||||
|
Ok(Some(json))
|
||||||
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
Loading…
Reference in New Issue