Compare commits

..

2 Commits

Author SHA1 Message Date
Daan Vanoverloop cdc056dadc
I love macros! 2022-09-05 20:16:13 +02:00
Daan Vanoverloop 277782f16b
Temp 2022-09-05 19:27:41 +02:00
5 changed files with 136 additions and 119 deletions

6
web/dist/index.html vendored
View File

@ -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-167b3e062ee4dd95_bg.wasm" as="fetch" type="application/wasm" crossorigin=""> <link rel="preload" href="/index-b24e51ea036df368_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-167b3e062ee4dd95.js"></head> <link rel="modulepreload" href="/index-b24e51ea036df368.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-167b3e062ee4dd95.js';init('/index-167b3e062ee4dd95_bg.wasm');</script></body></html> <script type="module">import init from '/index-b24e51ea036df368.js';init('/index-b24e51ea036df368_bg.wasm');</script></body></html>

View File

@ -11,10 +11,21 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt};
use web_sys::{Event, HtmlInputElement, InputEvent}; use web_sys::{Event, HtmlInputElement, InputEvent};
use yew::prelude::*; use yew::prelude::*;
#[derive(Clone, PartialEq)]
pub struct Binding<T> {
pub onchange: Callback<T>,
pub value: T,
}
impl<T> Binding<T> {
pub fn new(value: T, onchange: Callback<T>) -> Self {
Self { value, onchange }
}
}
#[derive(Clone, PartialEq, Properties)] #[derive(Clone, PartialEq, Properties)]
pub struct InputProps { pub struct InputProps {
pub value: String, pub bind: Binding<String>,
pub onchange: Callback<String>,
pub class: Classes, pub class: Classes,
} }
@ -29,9 +40,8 @@ fn get_value_from_input_event(e: InputEvent) -> String {
#[function_component(TextInput)] #[function_component(TextInput)]
pub fn text_input(props: &InputProps) -> Html { pub fn text_input(props: &InputProps) -> Html {
let InputProps { let InputProps {
value,
onchange,
class, class,
bind: Binding { onchange, value },
} = props.clone(); } = props.clone();
let oninput = Callback::from(move |input_event: InputEvent| { let oninput = Callback::from(move |input_event: InputEvent| {

View File

@ -1,4 +1,8 @@
use crate::components::{Page, RenderEvent}; use crate::{
components::{Page, RenderEvent},
init,
};
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*; use yew::prelude::*;
use yew_hooks::*; use yew_hooks::*;
@ -6,25 +10,18 @@ use crate::{clone, util::api_request};
#[function_component(EventsPage)] #[function_component(EventsPage)]
pub fn events_page() -> Html { pub fn events_page() -> Html {
let events: UseAsyncHandle<Vec<lan_party_core::event::Event>, _> = use_async(async move { let events = use_state(|| Vec::new());
api_request::<_, Vec<lan_party_core::event::Event>>("GET", "/event", Option::<()>::None)
init!(events => {
events.set(api_request::<_, Vec<lan_party_core::event::Event>>("GET", "/event", Option::<()>::None)
.await .await
.map(|inner| inner.unwrap()) .map(|inner| inner.unwrap())
.map_err(|_| "failed to load users") .unwrap())
}); });
clone!(events; use_effect_with_deps(move |_| {
if events.data.is_none() {
events.run();
}
|| ()
}, ()));
html! { html! {
<Page> <Page>
{ if let Some(events) = &events.data { { events.view() }
events.view()
} else { html! {} }}
</Page> </Page>
} }
} }

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
clone, clone_cb, bind, bind_change, bind_value, clone, clone_cb, clone_cb_spawn,
components::{Button, Loading, Page, Table, TextInput}, components::{Binding, Button, Loading, Page, Table, TextInput},
init,
util::api_request, util::api_request,
}; };
use lan_party_core::user::User; use lan_party_core::user::User;
@ -15,75 +16,51 @@ pub fn users_page() -> Html {
let new_username = use_state(|| String::new()); let new_username = use_state(|| String::new());
let score_edit: UseStateHandle<Option<usize>> = use_state(|| Option::None); let score_edit: UseStateHandle<Option<usize>> = use_state(|| Option::None);
let current_score = use_state(|| String::new()); let current_score = use_state(|| String::new());
let users: UseAsyncHandle<Vec<User>, _> = use_async(async move { let users = use_state(|| Vec::new());
api_request::<_, Vec<User>>("GET", "/user", Option::<()>::None)
init!(users => {
users.set(api_request::<_, Vec<User>>("GET", "/user", Option::<()>::None)
.await .await
.map(|inner| inner.unwrap()) .map(|inner| inner.unwrap())
.map_err(|_| "failed to load users") .unwrap());
}); });
clone!(users; use_effect_with_deps(move |_| { let oncheck = clone_cb_spawn!(score_edit, current_score, users => {
if users.data.is_none() { if let (Some(score_edit), Ok(score)) = (*score_edit, current_score.parse()) {
users.run(); let user: &User = &users[score_edit];
} api_request::<_, ()>("POST", &format!("/user/{}/score", user.name), Some(score))
|| () .await
}, ())); .unwrap();
let mut cloned = (*users).clone();
let oncheck = clone_cb!(score_edit, current_score, users; move |_| {
clone!(score_edit, users, current_score; {
spawn_local(async move {
if let (Some(score_edit), Some(users_inner)) = (*score_edit, &users.data) {
if let Ok(score) = current_score.parse() {
let user: &User = &users_inner[score_edit];
api_request::<_, ()>("POST", &format!("/user/{}/score", user.name), Some(score)).await.unwrap();
let mut cloned = users_inner.clone();
cloned[score_edit].score = score; cloned[score_edit].score = score;
users.update(cloned); users.set(cloned);
}
} }
score_edit.set(None); score_edit.set(None);
}); });
});
});
let onedit = clone_cb!(current_score, score_edit, users; i => move |_| { let onedit = clone_cb!(current_score, score_edit, users => i => move |_| {
if let Some(users) = &users.data {
let user: &User = &users[i]; let user: &User = &users[i];
current_score.set(user.score.to_string()); current_score.set(user.score.to_string());
score_edit.set(Some(i)); score_edit.set(Some(i));
}
}); });
let ondelete = clone_cb!(users; i => move |_| { let ondelete = clone_cb_spawn!(users => i => {
clone!(users; { let user: &User = &users[i];
spawn_local(async move {
if let Some(users_inner) = &users.data {
let user: &User = &users_inner[i];
api_request::<_, ()>("DELETE", &format!("/user/{}", user.name), Option::<()>::None).await.unwrap(); api_request::<_, ()>("DELETE", &format!("/user/{}", user.name), Option::<()>::None).await.unwrap();
let cloned = users_inner.iter().cloned().filter(|u| u.name != user.name).collect(); let cloned = users.iter().cloned().filter(|u| u.name != user.name).collect();
users.update(cloned); users.set(cloned);
}
});
});
}); });
let onadd = clone_cb!(new_username, users; move |_| { let onadd = clone_cb_spawn!(new_username, users => {
clone!(new_username, users; {
spawn_local(async move {
if let Some(users_inner) = &users.data {
let user = api_request::<String, User>("POST", "/user", Some((*new_username).clone())).await.unwrap(); let user = api_request::<String, User>("POST", "/user", Some((*new_username).clone())).await.unwrap();
let mut cloned = users_inner.clone(); let mut cloned = (*users).clone();
cloned.push(user.unwrap()); cloned.push(user.unwrap());
users.update(cloned); users.set(cloned);
}
});
});
}); });
html! { html! {
<Page> <Page>
<Table headers={headers.clone()} loading=false rows={vec![]}> <Table headers={headers.clone()} loading=false rows={vec![]}>
{ if let Some(users) = &users.data {
{users.iter().enumerate().map(move |(i, user)| html! { {users.iter().enumerate().map(move |(i, user)| html! {
<tr> <tr>
<td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400">{&user.name}</td> <td class="whitespace-nowrap px-3 py-4 text-sm text-slate-400">{&user.name}</td>
@ -93,8 +70,7 @@ pub fn users_page() -> Html {
<span class="inline-block"> <span class="inline-block">
<TextInput <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"} 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"}
onchange={clone_cb!(current_score; move |value| current_score.set(value))} bind={bind!(current_score)}
value={(*current_score).clone()}
/> />
</span> </span>
<Button icon={"mdi-check"} onclick={oncheck.clone()} /> <Button icon={"mdi-check"} onclick={oncheck.clone()} />
@ -113,20 +89,12 @@ pub fn users_page() -> Html {
</td> </td>
</tr> </tr>
}).collect::<Html>()} }).collect::<Html>()}
} else {
html! {
<div class="grid place-items-center">
<Loading />
</div>
}
}}
<tr> <tr>
<td class="whitespace-nowrap px-3 text-sm text-slate-400"> <td class="whitespace-nowrap px-3 text-sm text-slate-400">
<span class="inline-block"> <span class="inline-block">
<TextInput <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"} 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"}
onchange={clone_cb!(new_username; move |value| new_username.set(value))} bind={bind!(new_username)}
value={(*new_username).clone()}
/> />
</span> </span>
</td> </td>

View File

@ -77,7 +77,7 @@ pub async fn api_request<B: Serialize, R: for<'a> Deserialize<'a>>(
#[macro_export] #[macro_export]
macro_rules! clone { macro_rules! clone {
($($var:ident),* $(,)? ; $body:expr) => {{ ($($var:ident),* $(,)? => $body:expr) => {{
$(let $var = $var.clone();)* $(let $var = $var.clone();)*
$body $body
}}; }};
@ -85,16 +85,58 @@ macro_rules! clone {
#[macro_export] #[macro_export]
macro_rules! clone_cb { macro_rules! clone_cb {
($($var:ident),* ; $body:expr) => { ($($var:ident),* $(,)? => $body:expr) => {
clone!($($var,)* ; { clone!($($var,)* => {
Callback::from($body) Callback::from($body)
}) })
}; };
($($var:ident),* ; $($param:ident),* => $body:expr) => { ($($var:ident),* $(,)? => $($param:ident),* $(,)? => $body:expr) => {
clone!($($var,)* ; { clone!($($var,)* => {
move |$($param,)*| { move |$($param,)*| {
Callback::from($body) Callback::from($body)
} }
}) })
}; };
} }
#[macro_export]
macro_rules! clone_cb_spawn {
($($var:ident),* => $body:expr) => {
clone_cb!($($var,)* => move |_| {
clone!($($var,)* => spawn_local(async move { $body }))
})
};
($($var:ident),* $(,)? => $($param:ident),* => $body:expr) => {
clone_cb!($($var,)* => $($param),* => move |_| {
clone!($($var,)* => spawn_local(async move { $body }))
})
};
}
#[macro_export]
macro_rules! init {
($var:ident => $body:expr) => {
clone!($var => use_mount(move || spawn_local(async move { $body })))
};
}
#[macro_export]
macro_rules! bind_value {
($value:ident) => {
(*$value).clone()
};
}
#[macro_export]
macro_rules! bind_change {
($value:ident) => {
clone_cb!($value => move |value| $value.set(value))
};
}
#[macro_export]
macro_rules! bind {
($value:ident) => {
Binding::new(bind_value!($value), bind_change!($value))
};
}