From 59eeabc888faa126f9aa42670a650c47f3aeddcd Mon Sep 17 00:00:00 2001 From: Daan Vanoverloop Date: Tue, 13 Sep 2022 18:39:38 +0200 Subject: [PATCH] Temp --- backend/src/api/event.rs | 14 ++++ core/src/event.rs | 3 +- core/src/view.rs | 1 + macros/src/lib.rs | 162 ++++++++++++++++++++++++++++++------ web/dist/index.html | 8 +- web/src/components/mod.rs | 47 ++++++++++- web/src/pages/events.rs | 167 ++++++++++++++++++++++++++++++++------ web/src/util.rs | 10 ++- web/style.css | 20 ++++- 9 files changed, 375 insertions(+), 57 deletions(-) diff --git a/backend/src/api/event.rs b/backend/src/api/event.rs index f850169..b5c229c 100644 --- a/backend/src/api/event.rs +++ b/backend/src/api/event.rs @@ -14,6 +14,7 @@ api_routes!( update_event, get_all_events, event_outcome, + delete_event, ); pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection) -> Result<(), PartyError> { @@ -164,3 +165,16 @@ pub async fn stop_event( Ok(Json(outcome)) } + +/// # Delete event by name +#[openapi(tag = "Event")] +#[delete("/")] +pub async fn delete_event( + _api_key: ApiKey, + db: Connection, + name: String, +) -> Result { + db.events().delete_one(doc! { "name": name }, None).await?; + + Ok(Status::Ok) +} diff --git a/core/src/event.rs b/core/src/event.rs index a6141c5..c58b684 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -4,7 +4,7 @@ use crate::util::PartyError; #[cfg(feature = "sycamore")] use crate::view::prelude::*; #[cfg(feature = "sycamore")] -use lan_party_macros::{WebEdit, WebView}; +use lan_party_macros::{web_view_attr, WebEdit, WebView}; use paste::paste; #[cfg(feature = "openapi")] use schemars::JsonSchema; @@ -26,6 +26,7 @@ pub struct EventOutcome { #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebView))] +#[cfg_attr(feature = "sycamore", web_view_attr(title = "name"))] pub struct Event { /// Has this event concluded? #[cfg_attr(feature = "serde", serde(default))] diff --git a/core/src/view.rs b/core/src/view.rs index df7b07d..0b2fbab 100644 --- a/core/src/view.rs +++ b/core/src/view.rs @@ -174,6 +174,7 @@ impl<'a, G: Html, A: for<'b> Viewable<'b, G> + Clone, B: for<'b> Viewable<'b, G> view! { cx, Block(title="Tuple".into()) { (props.state.0.view(cx)) + br() (props.state.1.view(cx)) } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 0b20019..7c746a8 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -3,7 +3,16 @@ mod edit; use convert_case::{Case, Casing}; use proc_macro::TokenStream; use quote::{__private::TokenStream as TokenStream2, format_ident, quote}; -use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type}; +use syn::{ + parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Lit, + MetaNameValue, Path, Type, +}; + +enum ParsedAttribute { + Documentation(Documentation), + View(ViewAttribute), + None, +} #[derive(Debug)] enum Documentation { @@ -12,6 +21,11 @@ enum Documentation { None, } +enum ViewAttribute { + Title(Ident), + None, +} + impl Documentation { fn parse(attr: &Attribute) -> Documentation { if !attr.path.is_ident("doc") { @@ -30,33 +44,93 @@ impl Documentation { } } -fn get_title_description(attrs: &[Attribute]) -> (Option, Option) { - let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect(); +impl ViewAttribute { + fn parse(attr: &Attribute) -> ViewAttribute { + if !attr.path.is_ident("web_view_attr") { + return Self::None; + } - let mut title = None; - let mut description: Option = None; + let parsed: Result = parse_str( + attr.tokens + .to_string() + .trim_matches(|c: char| c == '(' || c == ')'), + ); - for doc in docs { - match doc { - Documentation::Title(t) => title = Some(t), - Documentation::Description(d) => { - if description.is_some() { - description.as_mut().unwrap().push(' '); - } else { - let _ = description.insert(String::new()); - } - description.as_mut().unwrap().push_str(&d); - } - _ => {} + dbg!(&parsed); + + match parsed { + Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) { + ("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())), + _ => Self::None, + }, + Err(_) => Self::None, } } +} - (title, description) +impl ParsedAttribute { + fn parse(attr: &Attribute) -> ParsedAttribute { + match attr.path.get_ident() { + Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)), + Some(i) if i.to_string() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)), + _ => Self::None, + } + } +} + +struct Attributes { + title: Option, + title_field: Option, + description: Option, +} + +impl Attributes { + pub fn parse(attrs: &[Attribute]) -> Self { + let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect(); + + let mut title = None; + let mut title_field = None; + let mut description: Option = None; + + for attr in parsed { + match attr { + ParsedAttribute::Documentation(doc) => match doc { + Documentation::Title(t) => title = Some(t), + Documentation::Description(d) => { + if description.is_some() { + description.as_mut().unwrap().push(' '); + } else { + let _ = description.insert(String::new()); + } + description.as_mut().unwrap().push_str(&d); + } + _ => {} + }, + ParsedAttribute::View(v) => match v { + ViewAttribute::Title(t) => title_field = Some(t), + _ => {} + }, + _ => {} + } + } + + Self { + title, + description, + title_field, + } + } +} + +#[proc_macro_attribute] +pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { + item } struct ItemProps { name: Ident, title: String, + title_field: Option, description: Option, } @@ -80,6 +154,7 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 { name, title, description, + .. } = props; let fields = s.fields.iter().map(|f| { @@ -89,7 +164,11 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 { .expect("each struct field must be named") .clone(); let name_str = name.to_string(); - let (title, description) = get_title_description(&f.attrs); + let Attributes { + title, + title_field: _, + description, + } = Attributes::parse(&f.attrs); StructField { name_str, @@ -161,6 +240,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 { name, title, description, + .. } = props; let variants = e.variants.iter().map(|v| { @@ -171,7 +251,11 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 { _ => unimplemented!(), }; - let (title, description) = get_title_description(&v.attrs); + let Attributes { + title, + title_field: _, + description, + } = Attributes::parse(&v.attrs); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); EnumVariant { variant_lower, @@ -288,6 +372,7 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 { name, title, description, + title_field, } = props; let fields = s.fields.iter().map(|f| { @@ -297,7 +382,11 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 { .expect("each struct field must be named") .clone(); let name_str = name.to_string(); - let (title, description) = get_title_description(&f.attrs); + let Attributes { + title, + title_field: _, + description, + } = Attributes::parse(&f.attrs); StructField { name_str, @@ -334,6 +423,16 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 { } }); + let title = if let Some(title_field) = title_field { + quote! { + state.#title_field + } + } else { + quote! { + #title + } + }; + quote! { let state = props.state; @@ -355,6 +454,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 { name, title, description, + .. } = props; let variants = e.variants.iter().map(|v| { @@ -365,7 +465,11 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 { _ => unimplemented!(), }; - let (title, description) = get_title_description(&v.attrs); + let Attributes { + title, + title_field: _, + description, + } = Attributes::parse(&v.attrs); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); EnumVariant { variant_lower, @@ -429,7 +533,11 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream { let name = input.ident; let edit_ident = format_ident!("{}Edit", name); - let (t, d) = get_title_description(&input.attrs); + let Attributes { + title: t, + title_field, + description: d, + } = Attributes::parse(&input.attrs); let title = t.unwrap_or(name.to_string()); let description = d; @@ -438,6 +546,7 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream { name: name.clone(), title: title.clone(), description: description.clone(), + title_field: title_field.clone(), }; let inner = match input.data { @@ -470,7 +579,11 @@ pub fn web_view(tokens: TokenStream) -> TokenStream { let name = input.ident; let view_ident = format_ident!("{}View", name); - let (t, d) = get_title_description(&input.attrs); + let Attributes { + title: t, + title_field, + description: d, + } = Attributes::parse(&input.attrs); let title = t.unwrap_or(name.to_string()); let description = d; @@ -479,6 +592,7 @@ pub fn web_view(tokens: TokenStream) -> TokenStream { name: name.clone(), title: title.clone(), description: description.clone(), + title_field: title_field.clone(), }; let inner = match input.data { diff --git a/web/dist/index.html b/web/dist/index.html index 2653ece..f7a7920 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -2,11 +2,11 @@ - + LAN Party - - + + - \ No newline at end of file + \ No newline at end of file diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index cf38d8a..514507c 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -1,6 +1,6 @@ pub mod messages; -use sycamore::prelude::*; +use sycamore::{builder::prelude::*, prelude::*}; use web_sys::Event; #[derive(Prop)] @@ -74,3 +74,48 @@ pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View { } } } + +/* +#[derive(Prop)] +pub struct TestProps<'a> { + pub text: &'a str, +} + +#[component] +pub fn Test<'a, G: Html>(cx: Scope<'a>, props: TestProps<'a>) -> View { + let text = create_ref(cx, props.text.clone()); + + // This is okay, but I don't know why + create_child_scope(cx, move |_| { + println!("{}", props.text); + drop(props.text); + }); + + // This is fine + create_child_scope(cx, move |_| { + println!("{}", text); + drop(text); + }); + + // Builders always seem to work just fine + let _: View = div().c(p().t(text)).view(cx); + let _: View = div().dyn_c_scoped(|cx| p().t(text).view(cx)).view(cx); + let _: View = div() + .dyn_c_scoped(|cx| p().t(props.text).view(cx)) + .t(props.text) + .view(cx); + + // error[E0521]: borrowed data escapes outside of function + let _: View = view! { cx, + p { (text) } + }; + + // error[E0521]: borrowed data escapes outside of function + let _: View = view! { cx, + p { (props.text) } + p { (props.text) } + }; + + view! { cx, } +} +*/ diff --git a/web/src/pages/events.rs b/web/src/pages/events.rs index 1fe272b..fae98e9 100644 --- a/web/src/pages/events.rs +++ b/web/src/pages/events.rs @@ -1,4 +1,5 @@ use lan_party_core::{ + components::Block, edit::IntoEdit, event::{Event, EventSpec, EventUpdate}, view::IntoView, @@ -8,7 +9,7 @@ use reqwasm::http::Method; use sycamore::{futures::spawn_local_scoped, prelude::*}; use crate::{ - components::{messages::Messenger, Block, Button}, + components::{messages::Messenger, Button}, util::api_request, }; @@ -18,17 +19,20 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View { let event_spec = create_signal(cx, EventSpec::default()); let event_update = create_signal(cx, EventUpdate::default()); + let event_update_name = create_signal(cx, String::new()); let events: &'a Signal> = create_signal(cx, Vec::::new()); - spawn_local_scoped(cx, async move { + let update_events = move || async move { events.set( api_request::<_, Vec>(Method::GET, "/event", Option::<()>::None) .await .map(|inner| inner.unwrap()) .unwrap(), ); - }); + }; + + spawn_local_scoped(cx, update_events()); let onadd = move |_| { spawn_local_scoped(cx, async move { @@ -54,29 +58,146 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View { }); }; - view! { cx, - Block(title="Events".into()) { - Keyed( - iterable=&events, - view=move |cx, event| { - let event = create_ref(cx, event); - view! { cx, - (event.view(cx)) - br() - } - }, - key=|event| (event.name.clone()), + let onupdate = move |_| { + spawn_local_scoped(cx, async move { + let res = api_request::( + Method::POST, + &format!("/event/{}", event_update_name), + Some((*event_update).get().as_ref().clone()), ) + .await; + + if let Ok(_) = res { + update_events().await; + messenger.info( + "Updated event", + format!( + "Successfully updated event with name \"{}\"", + event_update_name + ), + ); + } else { + messenger.info( + "Error when updating event", + format!("Unable to update event with name \"{}\"", event_update_name), + ); + } + }); + }; + + let onviewoutcome = move |event_name: String| { + move |_| { + let event_name = event_name.clone(); + spawn_local_scoped(cx, async move {}); } - br() - Block(title="Create new event".into()) { - (event_spec.edit(cx)) - Button(icon="mdi-check".into(), onclick=onadd) + }; + + let onstop = move |event_name: String| { + move |_| { + let event_name = event_name.clone(); + spawn_local_scoped(cx, async move {}); } - br() - Block(title="Update an event".into()) { - (event_update.edit(cx)) - Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get())) + }; + + let ondelete = move |event_name: String| { + move |_| { + let event_name = event_name.clone(); + spawn_local_scoped(cx, async move { + let res = + api_request::<(), ()>(Method::DELETE, &format!("/event/{}", event_name), None) + .await; + + if let Ok(_) = res { + update_events().await; + messenger.info( + "Removed event", + format!("Successfully removed event with name \"{}\"", event_name), + ); + } else { + messenger.info( + "Error when removing event", + format!("Unable to remove event with name \"{}\"", event_name), + ); + } + }); + } + }; + + view! { cx, + div(class="events-cols") { + div { + Block(title="Events".into()) { + Indexed( + iterable=&events, + view=move |cx, event| { + let event = create_ref(cx, event); + view! { cx, + //(event.view(cx)) + EventView(event=event, ondelete=ondelete(event.name.clone()), onviewoutcome=onviewoutcome(event.name.clone()), onstop=onstop(event.name.clone())) + br() + } + }, + ) + } + } + div(class="events-right") { + Block(title="Create new event".into()) { + (event_spec.edit(cx)) + Button(icon="mdi-check".into(), onclick=onadd) + } + br() + Block(title="Update an event".into()) { + label { "Event name" } + select(bind:value=event_update_name) { + Indexed( + iterable=&events, + view=move |cx, event| { + let event = create_ref(cx, event); + view! { cx, + option(value=event.name) { (event.name) } + } + }, + ) + } + (event_update.edit(cx)) + Button(icon="mdi-check".into(), onclick=onupdate) + } + } + } + } +} + +#[derive(Prop)] +struct EventViewProps<'a, F1, F2, F3> +where + F1: FnMut(web_sys::Event), + F2: FnMut(web_sys::Event), + F3: FnMut(web_sys::Event), +{ + pub event: &'a Event, + + pub ondelete: F1, + pub onviewoutcome: F2, + pub onstop: F3, +} + +#[component] +fn EventView<'a, G, F1, F2, F3>(cx: Scope<'a>, props: EventViewProps<'a, F1, F2, F3>) -> View +where + F1: FnMut(web_sys::Event) + 'a, + F2: FnMut(web_sys::Event) + 'a, + F3: FnMut(web_sys::Event) + 'a, + G: Html, +{ + view! { cx, + Block(title=props.event.name.clone()) { + (props.event.description) + br() + Button(text="Delete".into(), icon="mdi-delete".into(), onclick=props.ondelete) + Button(text="View outcome".into(), onclick=props.onviewoutcome) + Button(text="Finish".into(), onclick=props.onstop) + br() + (props.event.event_type.view(cx)) } } } diff --git a/web/src/util.rs b/web/src/util.rs index 93702c2..4a9d475 100644 --- a/web/src/util.rs +++ b/web/src/util.rs @@ -33,10 +33,14 @@ pub async fn api_request Deserialize<'a>>( let res = req.send().await?; - if let Ok(json) = res.json().await { - Ok(Some(json)) + if res.ok() { + if let Ok(json) = res.json().await { + Ok(Some(json)) + } else { + Ok(None) + } } else { - Ok(None) + Err(anyhow!("Request failed")) } } diff --git a/web/style.css b/web/style.css index 57a3544..5e04eec 100644 --- a/web/style.css +++ b/web/style.css @@ -64,7 +64,7 @@ textarea:focus, input:focus{ } body { - grid-template-columns: 1fr min(60rem, 90%) 1fr; + grid-template-columns: 1fr min(100rem, 90%) 1fr; } .messages { @@ -91,3 +91,21 @@ body { margin-bottom: 0; font-size: 1rem; } + +.events-cols { + display: grid; + grid-template-columns: 50% 50%; +} + +.events-cols > * { + padding-left: 1em; + padding-right: 1em; +} + +.events-cols:nth-child(1) { + grid-column: 1; +} + +.events-cols:nth-child(2) { + grid-column: 2; +}