From 5f0317c0fa024c3400aeb5c5749a6ef11d452ba8 Mon Sep 17 00:00:00 2001 From: Daan Vanoverloop Date: Wed, 7 Sep 2022 15:03:47 +0200 Subject: [PATCH] Sycamore? Sick of more? Sick of more Javascript! --- Cargo.lock | 178 ++++++++++++++ core/src/user.rs | 2 +- web/Cargo.toml | 11 + web/dist/index.html | 6 +- web/src/' | 139 +++++++++++ web/src/components/event.rs | 460 ++++-------------------------------- web/src/components/mod.rs | 184 +++++++-------- web/src/event_old.rs | 448 +++++++++++++++++++++++++++++++++++ web/src/main.rs | 190 +++++++-------- web/src/pages/events.rs | 49 ++-- web/src/pages/events_old.rs | 39 +++ web/src/pages/users.rs | 232 ++++++++++-------- web/src/pages/users_old.rs | 109 +++++++++ web/src/util.rs | 61 +---- 14 files changed, 1319 insertions(+), 789 deletions(-) create mode 100644 web/src/' create mode 100644 web/src/event_old.rs create mode 100644 web/src/pages/events_old.rs create mode 100644 web/src/pages/users_old.rs diff --git a/Cargo.lock b/Cargo.lock index dd928cd..10e9b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,16 @@ dependencies = [ "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]] name = "cookie" version = "0.16.0" @@ -770,6 +780,26 @@ dependencies = [ "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]] name = "gloo-render" version = "0.1.1" @@ -916,6 +946,15 @@ dependencies = [ "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]] name = "http" version = "0.2.8" @@ -1109,11 +1148,16 @@ name = "lan_party_web" version = "0.1.0" dependencies = [ "anyhow", + "console_log", "js-sys", "lan_party_core", + "log", "paste", + "reqwasm", "serde", "serde_json", + "sycamore", + "sycamore-router", "thiserror", "wasm-bindgen", "wasm-bindgen-futures", @@ -1706,6 +1750,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwasm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb" +dependencies = [ + "gloo-net", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2178,6 +2231,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -2349,6 +2411,116 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "syn" version = "1.0.99" @@ -2781,6 +2953,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + [[package]] name = "uuid" version = "0.8.2" diff --git a/core/src/user.rs b/core/src/user.rs index 8cc38d4..9480cd6 100644 --- a/core/src/user.rs +++ b/core/src/user.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; /// # User /// /// 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 = "serde", derive(Serialize, Deserialize))] pub struct User { diff --git a/web/Cargo.toml b/web/Cargo.toml index dad1bc1..3eeaf6b 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -7,7 +7,9 @@ edition = "2021" [dependencies] yew = "0.19" +#yew = { git = "https://github.com/yewstack/yew" } yew-router = "0.16" +#yew-router = { git = "https://github.com/yewstack/yew" } web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] } lan_party_core = { path = "../core", features = ["serde"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } @@ -16,6 +18,15 @@ serde = "1" serde_json = "1" thiserror = "1" yew-hooks = "0.1" +#yew-hooks = { git = "https://github.com/jetli/yew-hooks.git" } anyhow = "1.0" js-sys = "0.3" 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" + + + diff --git a/web/dist/index.html b/web/dist/index.html index b091628..aac155c 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -4,9 +4,9 @@ Yew App - - + + - \ No newline at end of file + \ No newline at end of file diff --git a/web/src/' b/web/src/' new file mode 100644 index 0000000..cbbf641 --- /dev/null +++ b/web/src/' @@ -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 { + let users = create_signal(cx, Vec::::new()); + + spawn_local_scoped(cx, async move { + users.set( + api_request::<_, Vec>(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 { + let children = props.children.call(cx); + + view! { cx, + div(class="max-w-7xl mx-auto") { (children) } + } +} + +#[derive(Prop)] +pub struct Props { + 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) -> View { + 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, + pub rows: Vec>, + pub loading: bool, + + #[builder(default)] + pub children: Children<'a, G>, +} + +#[component] +pub fn table<'a, G: Html>(props: TableProps<'a, G>) -> View { + 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! {} }) + } + } + } + } + } +} diff --git a/web/src/components/event.rs b/web/src/components/event.rs index a8bfb4b..a68f7d0 100644 --- a/web/src/components/event.rs +++ b/web/src/components/event.rs @@ -1,448 +1,92 @@ -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! { - <> - $( -

- { $name } - { $prop.view() } -

- )* - - } - }; -} - -macro_rules! view_struct { - ($struct:path: $title:expr => $(($name:expr, $prop:ident)),* $(,)?) => { - impl View for $struct { - fn view(&self) -> Html { - html! { - - { view_fields!( - $(($name, self.$prop),)* - )} - - } - } - } - }; - ($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! { - <> -
-

move |_| open.set(!*open))}>{ &props.title }

-
-
-
- {for props.children.iter()} -
-
-
- - } -} - -pub trait View { - fn view(&self) -> Html; -} - -impl View for bool { - fn view(&self) -> Html { - html! { - - } - } -} - -pub trait ViewPlain: Into + std::fmt::Display {} - -impl 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! { - -
    - { self.iter().map(|x| html! {
  • { x.view() }
  • }).collect::() } -
-
- } - } - } - }; -} - -view_iter!(T => Vec); -view_iter!(T => HashSet); -view_iter!(T => &[T]); - -impl View for Option { - fn view(&self) -> Html { - match self { - Some(content) => content.view(), - None => html! { "None" }, - } - } -} - -impl View for HashMap { - fn view(&self) -> Html { - html! { - - {self.iter().map(|(k, v)| { - html! {

{k.view()}{ ": " }{v.view()}

} - }).collect::()} -
- } - } -} - -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! { - - { view_fields!(("Ranking: ", self.ranking)) } - { view_fields!( - ("Participants: ", self.spec.participants), - ("Win rewards: ", self.spec.win_rewards), - ("Lose rewards: ", self.spec.lose_rewards), - ) } - - } -); - -view_enum_simple!( - lan_party_core::event::free_for_all_game::FreeForAllGameRanking: Ranking, - Scores -); - -view_struct!(TeamGame as self => - html! { - - { 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), - ) } - - } -); +use crate::components::Block; +use lan_party_core::event::EventSpec; +use sycamore::prelude::*; macro_rules! edit_fields { - ($(($name:expr, $prop:expr)),* $(,)?) => { - html! { - <> + ($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => { + view! { $cx, $( -

- { $name } - { $prop.edit() } -

+ p { + ($name) ": " ($prop.edit($cx)) + } )* - } }; } macro_rules! link_fields { - ($($field:ident),* $(,)? => $state:ident as $t:ident) => { - $(let $field = use_state(|| $state.$field.clone());)* + ($cx:ident, $($field:ident),* $(,)? => $state:ident as $t:ident) => { + $(let $field = create_signal($cx, $state.get().$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()), - ); + create_effect($cx, || { + $state.set(Self { + $($field: $field.get().as_ref().clone(),)* + ..Default::default() + }); + }); }; } 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! { - - { edit_fields!($(($name, $prop)),*) } - + impl<'a, G: Html> Edit<'a, G> for $struct { + fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View { + let state = props.state; + link_fields!(cx, $($prop,)* => state as Self); + view! { cx, + Block(title=stringify!($struct).into()) { + (edit_fields!(cx, $(($name, $prop),)*)) + } } } - - 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! { - - - { match &*state { - $($enum::$variant(_) => $var_name.edit(),)* - }} - - } - } - - impl Editable for $enum { - type Edit = [<$enum Edit>]; - } - } - }; +#[derive(Prop)] +pub struct EditProps<'a, T> { + pub state: &'a Signal, } -#[derive(PartialEq, Properties)] -pub struct EditProps { - pub state: UseStateHandle, -} - -pub trait Edit { - fn edit(&self) -> Html; -} - -impl>, Type: Editable + PartialEq> Edit - for UseStateHandle -{ - fn edit(&self) -> Html { - html!() +impl<'a, T> From<&'a Signal> for EditProps<'a, T> { + fn from(state: &'a Signal) -> Self { + EditProps { state } } } -pub trait Editable { - type Edit; +pub trait IntoEdit<'a, G: Html> { + fn edit(self, cx: Scope<'a>) -> View; } -#[function_component(InputEdit)] -pub fn input_edit(props: &EditProps) -> Html +impl<'a, G: Html, T: Edit<'a, G>> IntoEdit<'a, G> for &'a Signal where - T: PartialEq + Clone + 'static, - Binding: From>, + EditProps<'a, T>: From<&'a Signal>, { - let string = props.state.clone(); - - html! { - + fn edit(self, cx: Scope<'a>) -> View { + T::edit(cx, self.into()) } } -impl Editable for T -where - T: PartialEq + Clone + 'static, - Binding: From>, -{ - type Edit = InputEdit; +pub trait Edit<'a, G: Html>: Sized { + fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View; } -#[function_component(Stub)] -pub fn stub(props: &EditProps) -> Html { - html! { "stub" } -} - -impl Editable for Vec { - type Edit = Stub>; -} - -impl Editable for HashMap { - type Edit = Stub>; -} - -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, - 1: scores = Scores: HashMap, -); +edit_struct!(EventSpec => ("Name", name)); /* -#[function_component(EventTypeSpecEdit)] -pub fn event_type_spec_edit(props: &EditProps) -> 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! { - - - { match &*state { - EventTypeSpec::Test(_) => test.edit(), - EventTypeSpec::TeamGame(_) => team_game.edit(), - EventTypeSpec::FreeForAllGame(_) => free_for_all_game.edit(), - }} - +impl<'a, G: Html> Edit<'a, G> for EventSpec { + fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View { + let state = props.state; + link_fields!(cx, name => state as Self); + edit_fields!(cx, ("Name", name)) } } - -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 { + view! { cx, + input(bind:value=props.state) + } + } +} diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index 0dd3837..69e4ae4 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -1,120 +1,117 @@ -mod button; pub mod event; -mod table; -pub use button::Button; -pub use event::View; -pub use table::Table; -use yew::{function_component, html, Children, Properties}; +use sycamore::prelude::*; +use web_sys::Event; -use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; -use web_sys::{Event, HtmlInputElement, HtmlSelectElement, InputEvent}; -use yew::prelude::*; - -#[derive(Clone, PartialEq)] -pub struct Binding { - pub onchange: Callback, - pub value: T, +#[derive(Prop)] +pub struct PageProps<'a, G: Html> { + pub children: Children<'a, G>, } -impl Binding { - pub fn new(value: T, onchange: Callback) -> Self { - Self { value, onchange } +#[component] +pub fn Page<'a, G: Html>(cx: Scope<'a>, props: PageProps<'a, G>) -> View { + let children = props.children.call(cx); + + view! { cx, + div(class="max-w-7xl mx-auto") { (children) } } } -impl From> for Binding { - fn from(val: Binding) -> Self { - Binding { - onchange: val.onchange.reform(|s: String| s.parse().unwrap_or(0)), - value: val.value.to_string(), +#[derive(Prop)] +pub struct Props { + 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) -> View { + 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> for Binding { - fn from(val: Binding) -> Self { - Binding { - onchange: val.onchange.reform(|s: String| s.parse().unwrap_or(0)), - value: val.value.to_string(), +#[derive(Prop)] +pub struct TableProps<'a, G: Html> { + pub headers: Vec, + + pub children: Children<'a, G>, +} + +#[component] +pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View { + 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)] -pub struct InputProps { - pub bind: Binding, - pub class: Classes, +#[derive(Prop)] +pub struct BlockProps<'a, G: Html> { + title: String, + children: Children<'a, G>, } -fn get_value_from_input_event(e: InputEvent) -> String { - let event: Event = e.dyn_into().unwrap_throw(); - let event_target = event.target().unwrap_throw(); - let target: HtmlInputElement = event_target.dyn_into().unwrap_throw(); - target.value() -} +#[component] +pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View { + let children = props.children.call(cx); + let open = create_signal(cx, false); -#[function_component(TextInput)] -pub fn text_input(props: &InputProps) -> Html { - let InputProps { - class, - bind: Binding { onchange, value }, - } = props.clone(); - - let oninput = Callback::from(move |input_event: InputEvent| { - onchange.emit(get_value_from_input_event(input_event)); + let class = create_memo(cx, || { + if *open.get() { + "overflow-hidden" + } else { + "overflow-hidden max-h-1" + } }); - html! { - + view! { cx, + 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, - 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! { - - } -} - -#[derive(Properties, PartialEq, Default)] -pub struct PageProps { - #[prop_or_default] - pub children: Children, -} - -#[function_component(Page)] -pub fn page(props: &PageProps) -> Html { - html! {
{ for props.children.iter() }
} -} - +/* #[function_component(Loading)] pub fn loading() -> Html { html! { @@ -129,3 +126,4 @@ pub fn loading() -> Html { } } +*/ diff --git a/web/src/event_old.rs b/web/src/event_old.rs new file mode 100644 index 0000000..9f70a58 --- /dev/null +++ b/web/src/event_old.rs @@ -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! { + <> + $( +

+ { $name } + { $prop.view() } +

+ )* + + } + }; +} + +macro_rules! view_struct { + ($struct:path: $title:expr => $(($name:expr, $prop:ident)),* $(,)?) => { + impl View for $struct { + fn view(&self) -> Html { + html! { + + { view_fields!( + $(($name, self.$prop),)* + )} + + } + } + } + }; + ($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! { + <> +
+

move |_| open.set(!*open))}>{ &props.title }

+
+
+
+ {for props.children.iter()} +
+
+
+ + } +} + +pub trait View { + fn view(&self) -> Html; +} + +impl View for bool { + fn view(&self) -> Html { + html! { + + } + } +} + +pub trait ViewPlain: Into + std::fmt::Display {} + +impl 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! { + +
    + { self.iter().map(|x| html! {
  • { x.view() }
  • }).collect::() } +
+
+ } + } + } + }; +} + +view_iter!(T => Vec); +view_iter!(T => HashSet); +view_iter!(T => &[T]); + +impl View for Option { + fn view(&self) -> Html { + match self { + Some(content) => content.view(), + None => html! { "None" }, + } + } +} + +impl View for HashMap { + fn view(&self) -> Html { + html! { + + {self.iter().map(|(k, v)| { + html! {

{k.view()}{ ": " }{v.view()}

} + }).collect::()} +
+ } + } +} + +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! { + + { view_fields!(("Ranking: ", self.ranking)) } + { view_fields!( + ("Participants: ", self.spec.participants), + ("Win rewards: ", self.spec.win_rewards), + ("Lose rewards: ", self.spec.lose_rewards), + ) } + + } +); + +view_enum_simple!( + lan_party_core::event::free_for_all_game::FreeForAllGameRanking: Ranking, + Scores +); + +view_struct!(TeamGame as self => + html! { + + { 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), + ) } + + } +); + +macro_rules! edit_fields { + ($(($name:expr, $prop:expr)),* $(,)?) => { + html! { + <> + $( +

+ { $name } + { $prop.edit() } +

+ )* + + } + }; +} + +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! { + + { edit_fields!($(($name, $prop)),*) } + + } + } + + 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! { + + + { match &*state { + $($enum::$variant(_) => $var_name.edit(),)* + }} + + } + } + + impl Editable for $enum { + type Edit = [<$enum Edit>]; + } + } + }; +} + +#[derive(PartialEq, Properties)] +pub struct EditProps { + pub state: UseStateHandle, +} + +pub trait Edit { + fn edit(&self) -> Html; +} + +impl>, Type: Editable + PartialEq> Edit + for UseStateHandle +{ + fn edit(&self) -> Html { + html!() + } +} + +pub trait Editable { + type Edit: Component; +} + +#[function_component(InputEdit)] +pub fn input_edit(props: &EditProps) -> Html +where + T: PartialEq + Clone + 'static, + Binding: From>, +{ + let string = props.state.clone(); + + html! { + + } +} + +impl Editable for T +where + T: PartialEq + Clone + 'static, + Binding: From>, +{ + type Edit = InputEdit; +} + +#[function_component(Stub)] +pub fn stub(props: &EditProps) -> Html { + html! { "stub" } +} + +impl Editable for Vec { + type Edit = Stub>; +} + +impl Editable for HashMap { + type Edit = Stub>; +} + +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, + 1: scores = Scores: HashMap, +); + +/* +#[function_component(EventTypeSpecEdit)] +pub fn event_type_spec_edit(props: &EditProps) -> 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! { + + + { match &*state { + EventTypeSpec::Test(_) => test.edit(), + EventTypeSpec::TeamGame(_) => team_game.edit(), + EventTypeSpec::FreeForAllGame(_) => free_for_all_game.edit(), + }} + + } +} + +impl Editable for EventTypeSpec { + type Edit = EventTypeSpecEdit; +} +*/ diff --git a/web/src/main.rs b/web/src/main.rs index 4a5b4aa..e4580e6 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -3,131 +3,103 @@ mod pages; pub mod util; use pages::{EventsPage, UsersPage}; -use yew::prelude::*; -use yew_router::prelude::*; +use sycamore::prelude::*; +use sycamore_router::{HistoryIntegration, Route, Router}; -#[derive(Clone, Debug, Routable, PartialEq)] -enum Route { - #[at("/")] +#[derive(Route)] +enum AppRoutes { + #[to("/")] Home, - #[at("/users")] + #[to("/users")] Users, - #[at("/events")] + #[to("/events")] Events, -} - -struct Model { - pages: Vec, -} - -impl Component for Model { - type Message = (); - type Properties = (); - - fn create(_ctx: &Context) -> 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) -> Html { - html! { - - - render={Switch::render(switch)} /> - - } - } -} - -fn switch(routes: &Route) -> Html { - match routes { - Route::Home => html! {

{ "Home" }

}, - Route::Users => html! { - - }, - Route::Events => html! { }, - } + #[not_found] + NotFound, } #[derive(Clone, Debug, PartialEq)] -struct Page { +pub struct Page { name: String, - target: Route, + target: String, } -#[derive(Properties, PartialEq, Default)] -struct NavbarProps { - #[prop_or_default] - pub children: Children, - - pub pages: Vec, +#[derive(Prop)] +pub struct NavbarProps<'a, G: Html> { + children: Children<'a, G>, + pages: Vec, } -#[function_component(Navbar)] -fn navbar(props: &NavbarProps) -> Html { - let active_target = use_state(|| 0); - - let history = use_history().unwrap(); - - let onclick = |i: usize| { - let active_target = active_target.clone(); - let history = history.clone(); - let route = props.pages[i].target.clone(); - - Callback::from(move |_| { - history.push(route.clone()); - active_target.set(i) - }) - }; - - html! { - + (children) + } + } } } fn main() { - yew::start_app::(); + 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| { + 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" + }, + }) + } + } + } + ) + } + }); } diff --git a/web/src/pages/events.rs b/web/src/pages/events.rs index c2d0dd3..9a10602 100644 --- a/web/src/pages/events.rs +++ b/web/src/pages/events.rs @@ -1,39 +1,22 @@ -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 log::debug; +use sycamore::prelude::*; -use crate::{clone, clone_cb, util::api_request}; +use crate::components::{ + event::{Edit, EditProps}, + Block, Button, Page, +}; -#[function_component(EventsPage)] -pub fn events_page() -> Html { - let events = use_state(|| Vec::new()); +#[component] +pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View { + let event_spec = create_signal(cx, EventSpec::default()); - init!(events => { - events.set(api_request::<_, Vec>("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! { - - { events.view() } - - { event_spec.edit() } - -