diff --git a/web/dist/index.html b/web/dist/index.html index aac155c..01963f2 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/components/event.rs b/web/src/components/event.rs index a68f7d0..c2ecfac 100644 --- a/web/src/components/event.rs +++ b/web/src/components/event.rs @@ -1,7 +1,23 @@ +use std::str::FromStr; + use crate::components::Block; -use lan_party_core::event::EventSpec; +use lan_party_core::event::{ + free_for_all_game::FreeForAllGameSpec, team_game::TeamGameSpec, test::TestSpec, EventSpec, + EventTypeSpec, +}; +use paste::paste; use sycamore::prelude::*; +use super::input_classes; + +macro_rules! editable { + ($type:ty => $editor:ty) => { + impl<'a, G: Html> Editable<'a, G> for $type { + type Editor = $editor; + } + }; +} + macro_rules! edit_fields { ($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => { view! { $cx, @@ -19,7 +35,7 @@ macro_rules! link_fields { $(let $field = create_signal($cx, $state.get().$field.clone());)* create_effect($cx, || { - $state.set(Self { + $state.set($t { $($field: $field.get().as_ref().clone(),)* ..Default::default() }); @@ -29,16 +45,22 @@ macro_rules! link_fields { macro_rules! edit_struct { ($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => { - 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),)*)) + paste! { + pub struct [<$struct Edit>]; + + impl<'a, G: Html> Editor<'a, G, $struct> for [<$struct Edit>] { + fn edit(cx: Scope<'a>, props: EditProps<'a, $struct>) -> View { + let state = props.state; + link_fields!(cx, $($prop,)* => state as $struct); + view! { cx, + Block(title=stringify!($struct).into()) { + (edit_fields!(cx, $(($name, $prop),)*)) + } } } } + + editable!($struct => [<$struct Edit>]); } }; } @@ -58,7 +80,7 @@ pub trait IntoEdit<'a, G: Html> { fn edit(self, cx: Scope<'a>) -> View; } -impl<'a, G: Html, T: Edit<'a, G>> IntoEdit<'a, G> for &'a Signal +impl<'a, G: Html, T: Editable<'a, G>> IntoEdit<'a, G> for &'a Signal where EditProps<'a, T>: From<&'a Signal>, { @@ -71,22 +93,92 @@ pub trait Edit<'a, G: Html>: Sized { fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View; } -edit_struct!(EventSpec => ("Name", name)); - -/* -impl<'a, G: Html> Edit<'a, G> for EventSpec { +impl<'a, G: Html, E: Editor<'a, G, Type>, Type: Editable<'a, G, Editor = E>> Edit<'a, G> for Type { 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)) + E::edit(cx, props) } } -*/ -impl<'a, G: Html> Edit<'a, G> for String { - fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View { +pub trait Editor<'a, G: Html, Type>: Sized { + fn edit(cx: Scope<'a>, props: EditProps<'a, Type>) -> View; +} + +pub trait Editable<'a, G: Html>: Sized { + type Editor: Editor<'a, G, Self>; +} + +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 => ); + +pub struct StringEdit; + +impl<'a, G: Html> Editor<'a, G, String> for StringEdit { + fn edit(cx: Scope<'a>, props: EditProps<'a, String>) -> View { view! { cx, - input(bind:value=props.state) + input(class=input_classes(), bind:value=props.state) } } } + +editable!(String => StringEdit); + +pub struct StubEdit; + +impl<'a, G: Html, T> Editor<'a, G, T> for StubEdit +where + T: Editable<'a, G, Editor = StubEdit>, +{ + fn edit(cx: Scope<'a>, _props: EditProps<'a, T>) -> View { + view! { cx, + "Unimplemented" + } + } +} + +editable!(EventTypeSpec => StubEdit); + +pub struct InputEdit; + +impl<'a, G: Html, T> Editor<'a, G, T> for InputEdit +where + T: Editable<'a, G, Editor = InputEdit> + FromStr + ToString + Default, +{ + fn edit(cx: Scope<'a>, props: EditProps<'a, T>) -> View { + let value = create_signal(cx, props.state.get().to_string()); + + create_memo(cx, || { + props + .state + .set((*value.get()).parse().unwrap_or(T::default())) + }); + + view! { cx, + input(class=input_classes(), bind:value=value) + } + } +} + +editable!(i64 => InputEdit); +editable!(i32 => InputEdit); +editable!(isize => InputEdit); + +editable!(u64 => InputEdit); +editable!(u32 => InputEdit); +editable!(usize => InputEdit); + +editable!(f64 => InputEdit); +editable!(f32 => InputEdit); + +pub struct BoolEdit; + +impl<'a, G: Html> Editor<'a, G, bool> for BoolEdit { + fn edit(cx: Scope<'a>, props: EditProps<'a, bool>) -> View { + view! { cx, + input(type="checkbox", bind:checked=props.state) + } + } +} + +editable!(bool => BoolEdit); diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index 69e4ae4..b0fea69 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -111,6 +111,10 @@ pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View { } } +pub fn input_classes() -> &'static str { + "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" +} + /* #[function_component(Loading)] pub fn loading() -> Html { diff --git a/web/src/pages/users.rs b/web/src/pages/users.rs index e7b65fb..565ee68 100644 --- a/web/src/pages/users.rs +++ b/web/src/pages/users.rs @@ -1,4 +1,4 @@ -use crate::components::{Button, Page, Table}; +use crate::components::{input_classes, Button, Page, Table}; use lan_party_core::user::User; use log::debug; use reqwasm::http::Method; @@ -112,7 +112,7 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View { td(class="whatespace-nowrap px-3 text-sm") { (if Some(&user.name) == (*score_edit.get()).as_ref() { view! { cx, 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") + input(bind:value=new_score, class=input_classes()) } Button(icon="mdi-check".into(), onclick=oncheck) }} else { view! { cx, @@ -139,7 +139,7 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View { tr { td(class="whatespace-nowrap px-3 text-sm") { span(class="inline-block") { - 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") + input(bind:value=new_username, class=input_classes()) } } td(class="whatespace-nowrap px-3 py-4 text-sm") {}