This commit is contained in:
Daan Vanoverloop 2022-09-13 18:39:38 +02:00
parent 44745b252a
commit 59eeabc888
Signed by: Danacus
GPG Key ID: F2272B50E129FC5C
9 changed files with 375 additions and 57 deletions

View File

@ -14,6 +14,7 @@ api_routes!(
update_event, update_event,
get_all_events, get_all_events,
event_outcome, event_outcome,
delete_event,
); );
pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection<Db>) -> Result<(), PartyError> { pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection<Db>) -> Result<(), PartyError> {
@ -164,3 +165,16 @@ pub async fn stop_event(
Ok(Json(outcome)) Ok(Json(outcome))
} }
/// # Delete event by name
#[openapi(tag = "Event")]
#[delete("/<name>")]
pub async fn delete_event(
_api_key: ApiKey,
db: Connection<Db>,
name: String,
) -> Result<Status, PartyError> {
db.events().delete_one(doc! { "name": name }, None).await?;
Ok(Status::Ok)
}

View File

@ -4,7 +4,7 @@ use crate::util::PartyError;
#[cfg(feature = "sycamore")] #[cfg(feature = "sycamore")]
use crate::view::prelude::*; use crate::view::prelude::*;
#[cfg(feature = "sycamore")] #[cfg(feature = "sycamore")]
use lan_party_macros::{WebEdit, WebView}; use lan_party_macros::{web_view_attr, WebEdit, WebView};
use paste::paste; use paste::paste;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use schemars::JsonSchema; use schemars::JsonSchema;
@ -26,6 +26,7 @@ pub struct EventOutcome {
#[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))]
#[cfg_attr(feature = "sycamore", derive(WebView))] #[cfg_attr(feature = "sycamore", derive(WebView))]
#[cfg_attr(feature = "sycamore", web_view_attr(title = "name"))]
pub struct Event { pub struct Event {
/// Has this event concluded? /// Has this event concluded?
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]

View File

@ -174,6 +174,7 @@ impl<'a, G: Html, A: for<'b> Viewable<'b, G> + Clone, B: for<'b> Viewable<'b, G>
view! { cx, view! { cx,
Block(title="Tuple".into()) { Block(title="Tuple".into()) {
(props.state.0.view(cx)) (props.state.0.view(cx))
br()
(props.state.1.view(cx)) (props.state.1.view(cx))
} }
} }

View File

@ -3,7 +3,16 @@ mod edit;
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{__private::TokenStream as TokenStream2, format_ident, quote}; 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)] #[derive(Debug)]
enum Documentation { enum Documentation {
@ -12,6 +21,11 @@ enum Documentation {
None, None,
} }
enum ViewAttribute {
Title(Ident),
None,
}
impl Documentation { impl Documentation {
fn parse(attr: &Attribute) -> Documentation { fn parse(attr: &Attribute) -> Documentation {
if !attr.path.is_ident("doc") { if !attr.path.is_ident("doc") {
@ -30,14 +44,57 @@ impl Documentation {
} }
} }
fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>) { impl ViewAttribute {
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect(); fn parse(attr: &Attribute) -> ViewAttribute {
if !attr.path.is_ident("web_view_attr") {
return Self::None;
}
let parsed: Result<MetaNameValue, _> = parse_str(
attr.tokens
.to_string()
.trim_matches(|c: char| c == '(' || c == ')'),
);
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,
}
}
}
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<String>,
title_field: Option<Ident>,
description: Option<String>,
}
impl Attributes {
pub fn parse(attrs: &[Attribute]) -> Self {
let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect();
let mut title = None; let mut title = None;
let mut title_field = None;
let mut description: Option<String> = None; let mut description: Option<String> = None;
for doc in docs { for attr in parsed {
match doc { match attr {
ParsedAttribute::Documentation(doc) => match doc {
Documentation::Title(t) => title = Some(t), Documentation::Title(t) => title = Some(t),
Documentation::Description(d) => { Documentation::Description(d) => {
if description.is_some() { if description.is_some() {
@ -48,15 +105,32 @@ fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>
description.as_mut().unwrap().push_str(&d); description.as_mut().unwrap().push_str(&d);
} }
_ => {} _ => {}
},
ParsedAttribute::View(v) => match v {
ViewAttribute::Title(t) => title_field = Some(t),
_ => {}
},
_ => {}
} }
} }
(title, description) Self {
title,
description,
title_field,
}
}
}
#[proc_macro_attribute]
pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
} }
struct ItemProps { struct ItemProps {
name: Ident, name: Ident,
title: String, title: String,
title_field: Option<Ident>,
description: Option<String>, description: Option<String>,
} }
@ -80,6 +154,7 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
name, name,
title, title,
description, description,
..
} = props; } = props;
let fields = s.fields.iter().map(|f| { 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") .expect("each struct field must be named")
.clone(); .clone();
let name_str = name.to_string(); 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 { StructField {
name_str, name_str,
@ -161,6 +240,7 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
name, name,
title, title,
description, description,
..
} = props; } = props;
let variants = e.variants.iter().map(|v| { let variants = e.variants.iter().map(|v| {
@ -171,7 +251,11 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
_ => unimplemented!(), _ => 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)); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
EnumVariant { EnumVariant {
variant_lower, variant_lower,
@ -288,6 +372,7 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
name, name,
title, title,
description, description,
title_field,
} = props; } = props;
let fields = s.fields.iter().map(|f| { 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") .expect("each struct field must be named")
.clone(); .clone();
let name_str = name.to_string(); 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 { StructField {
name_str, 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! { quote! {
let state = props.state; let state = props.state;
@ -355,6 +454,7 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
name, name,
title, title,
description, description,
..
} = props; } = props;
let variants = e.variants.iter().map(|v| { let variants = e.variants.iter().map(|v| {
@ -365,7 +465,11 @@ fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
_ => unimplemented!(), _ => 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)); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
EnumVariant { EnumVariant {
variant_lower, variant_lower,
@ -429,7 +533,11 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
let name = input.ident; let name = input.ident;
let edit_ident = format_ident!("{}Edit", name); 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 title = t.unwrap_or(name.to_string());
let description = d; let description = d;
@ -438,6 +546,7 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
name: name.clone(), name: name.clone(),
title: title.clone(), title: title.clone(),
description: description.clone(), description: description.clone(),
title_field: title_field.clone(),
}; };
let inner = match input.data { let inner = match input.data {
@ -470,7 +579,11 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
let name = input.ident; let name = input.ident;
let view_ident = format_ident!("{}View", name); 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 title = t.unwrap_or(name.to_string());
let description = d; let description = d;
@ -479,6 +592,7 @@ pub fn web_view(tokens: TokenStream) -> TokenStream {
name: name.clone(), name: name.clone(),
title: title.clone(), title: title.clone(),
description: description.clone(), description: description.clone(),
title_field: title_field.clone(),
}; };
let inner = match input.data { let inner = match input.data {

8
web/dist/index.html vendored
View File

@ -2,11 +2,11 @@
<meta charset="utf-8"> <meta charset="utf-8">
<!--<link data-trunk href="tailwind.css" rel="css">--> <!--<link data-trunk href="tailwind.css" rel="css">-->
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css"> <link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
<link rel="stylesheet" href="/style-d252283caa890f82.css"> <link rel="stylesheet" href="/style-2f979acf99c8ad73.css">
<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>LAN Party</title> <title>LAN Party</title>
<link rel="preload" href="/index-50210a46cec614af_bg.wasm" as="fetch" type="application/wasm" crossorigin=""> <link rel="preload" href="/index-b0d435005316ee2a_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-50210a46cec614af.js"></head> <link rel="modulepreload" href="/index-b0d435005316ee2a.js"></head>
<body> <body>
<script type="module">import init from '/index-50210a46cec614af.js';init('/index-50210a46cec614af_bg.wasm');</script></body></html> <script type="module">import init from '/index-b0d435005316ee2a.js';init('/index-b0d435005316ee2a_bg.wasm');</script></body></html>

View File

@ -1,6 +1,6 @@
pub mod messages; pub mod messages;
use sycamore::prelude::*; use sycamore::{builder::prelude::*, prelude::*};
use web_sys::Event; use web_sys::Event;
#[derive(Prop)] #[derive(Prop)]
@ -74,3 +74,48 @@ pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
} }
} }
} }
/*
#[derive(Prop)]
pub struct TestProps<'a> {
pub text: &'a str,
}
#[component]
pub fn Test<'a, G: Html>(cx: Scope<'a>, props: TestProps<'a>) -> View<G> {
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<G> = div().c(p().t(text)).view(cx);
let _: View<G> = div().dyn_c_scoped(|cx| p().t(text).view(cx)).view(cx);
let _: View<G> = 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<G> = view! { cx,
p { (text) }
};
// error[E0521]: borrowed data escapes outside of function
let _: View<G> = view! { cx,
p { (props.text) }
p { (props.text) }
};
view! { cx, }
}
*/

View File

@ -1,4 +1,5 @@
use lan_party_core::{ use lan_party_core::{
components::Block,
edit::IntoEdit, edit::IntoEdit,
event::{Event, EventSpec, EventUpdate}, event::{Event, EventSpec, EventUpdate},
view::IntoView, view::IntoView,
@ -8,7 +9,7 @@ use reqwasm::http::Method;
use sycamore::{futures::spawn_local_scoped, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use crate::{ use crate::{
components::{messages::Messenger, Block, Button}, components::{messages::Messenger, Button},
util::api_request, util::api_request,
}; };
@ -18,17 +19,20 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
let event_spec = create_signal(cx, EventSpec::default()); let event_spec = create_signal(cx, EventSpec::default());
let event_update = create_signal(cx, EventUpdate::default()); let event_update = create_signal(cx, EventUpdate::default());
let event_update_name = create_signal(cx, String::new());
let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new()); let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new());
spawn_local_scoped(cx, async move { let update_events = move || async move {
events.set( events.set(
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None) api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
.await .await
.map(|inner| inner.unwrap()) .map(|inner| inner.unwrap())
.unwrap(), .unwrap(),
); );
}); };
spawn_local_scoped(cx, update_events());
let onadd = move |_| { let onadd = move |_| {
spawn_local_scoped(cx, async move { spawn_local_scoped(cx, async move {
@ -54,29 +58,146 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
}); });
}; };
let onupdate = move |_| {
spawn_local_scoped(cx, async move {
let res = api_request::<EventUpdate, ()>(
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 {});
}
};
let onstop = move |event_name: String| {
move |_| {
let event_name = event_name.clone();
spawn_local_scoped(cx, async move {});
}
};
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, view! { cx,
div(class="events-cols") {
div {
Block(title="Events".into()) { Block(title="Events".into()) {
Keyed( Indexed(
iterable=&events, iterable=&events,
view=move |cx, event| { view=move |cx, event| {
let event = create_ref(cx, event); let event = create_ref(cx, event);
view! { cx, view! { 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() br()
} }
}, },
key=|event| (event.name.clone()),
) )
} }
br() }
div(class="events-right") {
Block(title="Create new event".into()) { Block(title="Create new event".into()) {
(event_spec.edit(cx)) (event_spec.edit(cx))
Button(icon="mdi-check".into(), onclick=onadd) Button(icon="mdi-check".into(), onclick=onadd)
} }
br() br()
Block(title="Update an event".into()) { 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)) (event_update.edit(cx))
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get())) 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<G>
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))
} }
} }
} }

View File

@ -33,11 +33,15 @@ pub async fn api_request<B: Serialize, R: for<'a> Deserialize<'a>>(
let res = req.send().await?; let res = req.send().await?;
if res.ok() {
if let Ok(json) = res.json().await { if let Ok(json) = res.json().await {
Ok(Some(json)) Ok(Some(json))
} else { } else {
Ok(None) Ok(None)
} }
} else {
Err(anyhow!("Request failed"))
}
} }
#[macro_export] #[macro_export]

View File

@ -64,7 +64,7 @@ textarea:focus, input:focus{
} }
body { body {
grid-template-columns: 1fr min(60rem, 90%) 1fr; grid-template-columns: 1fr min(100rem, 90%) 1fr;
} }
.messages { .messages {
@ -91,3 +91,21 @@ body {
margin-bottom: 0; margin-bottom: 0;
font-size: 1rem; 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;
}