diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2022-12-23 11:29:37 -0500 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2022-12-23 11:30:01 -0500 |
commit | 6c9651194fda7a9167157e835fbe9fd691e9a1a9 (patch) | |
tree | 4e24f243bde8923f15b125db863ba58732617251 | |
parent | 868703627bfd27925a53fcfbdd3dbeef831660c8 (diff) |
Replace table with a struct component
This makes async & state handling much easier
-rw-r--r-- | Cargo.lock | 28 | ||||
-rw-r--r-- | webapp/Cargo.toml | 1 | ||||
-rw-r--r-- | webapp/src/components/app_context_provider.rs | 6 | ||||
-rw-r--r-- | webapp/src/components/table.rs | 228 | ||||
-rw-r--r-- | webapp/src/main.rs | 3 | ||||
-rw-r--r-- | webapp/src/services.rs | 36 |
6 files changed, 204 insertions, 98 deletions
@@ -549,6 +549,21 @@ dependencies = [ ] [[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-channel" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -565,6 +580,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-intrusive" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -610,6 +636,7 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2708,6 +2735,7 @@ dependencies = [ "console_error_panic_hook", "dotenv", "env_logger", + "futures", "getrandom", "gloo-net", "lazy_static", diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index f90e981..1407fe7 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -21,6 +21,7 @@ web-sys = { version = "0.3.60", features = ["Location", "Document"] } uuid = { version = "1.2.1", features = ["serde"] } rand = "0.8.4" serde = "1.0.147" +futures = "0.3.25" [dev-dependencies] env_logger = "0.10.0" diff --git a/webapp/src/components/app_context_provider.rs b/webapp/src/components/app_context_provider.rs index 7ca0cf3..6298c2b 100644 --- a/webapp/src/components/app_context_provider.rs +++ b/webapp/src/components/app_context_provider.rs @@ -51,6 +51,12 @@ impl AppContext { self.state.error.as_ref() } + pub fn set_error(&self, error: anyhow::Error) { + self.state.error.set(Some(ErrorInfoProperties { + message: format!("{error:?}"), + })); + } + pub fn create_table(&self) { let user = self.state.user.clone(); let history = self.history.clone(); diff --git a/webapp/src/components/table.rs b/webapp/src/components/table.rs index 92d862c..bf66897 100644 --- a/webapp/src/components/table.rs +++ b/webapp/src/components/table.rs @@ -1,114 +1,146 @@ -use std::future::Future; -use std::pin::Pin; - +use crate::components::AppContext; use crate::components::{BiddingBox, BiddingTable, Hand, TrickInPlay}; -use crate::use_app_context; -use crate::utils::ok_json; -use anyhow::Context; -use gloo_net::http::Request; +use crate::{services, use_app_context}; +use futures::FutureExt; use log::info; use protocol::bridge_engine::{ - Bid, BiddingState, BiddingStatePlayerView, GameStatePlayerView, - PlayStatePlayerView, + Bid, BiddingStatePlayerView, GameStatePlayerView, + PlayStatePlayerView, Player, }; +use protocol::card::Card; use yew::prelude::*; +#[derive(PartialEq, Properties, Clone)] +pub struct OnlineTableProps { + pub table: protocol::Table, +} + #[function_component(OnlineTable)] pub fn online_table(props: &OnlineTableProps) -> Html { let ctx = use_app_context(); + html! { + <OnlineTableInner table={props.table.clone()} app_ctx={ctx}/> + } +} - let table_state: UseStateHandle<Option<GameStatePlayerView>> = - use_state(|| None); - - let update_table_state = { - let table_state = table_state.clone(); - let props = props.clone(); - || { - Box::pin(async move { - // let table_state = table_state.clone(); - let props = props.clone(); - let response = - Request::get(&format!("/api/table/{}", props.table.id)) - .send() - .await - .context("fetching table data")?; - let table = ok_json(response).await?; - table_state.set(Some(table)); - Ok(()) - }) - as Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> - } - }; +#[derive(PartialEq, Properties, Clone)] +pub struct OnlineTableInnerProps { + pub table: protocol::Table, + pub app_ctx: AppContext, +} + +struct OnlineTableInner { + table_state: Option<GameStatePlayerView>, +} + +pub enum Msg { + TableStateUpdated(Result<GameStatePlayerView, anyhow::Error>), + Bid(Bid), + Play(Card), +} + +impl OnlineTableInner { + fn play(&mut self, ctx: &yew::Context<Self>, card: Card) { + let _play_state = match &self.table_state { + Some(GameStatePlayerView::Playing(play_state)) => play_state, + _ => { + info!( + "Cannot play card with table state: {:#?}", + self.table_state + ); + return; + } + }; + info!("Playing card {card:?}"); + let table = ctx.props().table.clone(); + ctx.link().send_future( + async move { + services::play(table.clone(), card).await?; + services::get_table_player_view(table).await + } + .map(Msg::TableStateUpdated), + ); + } +} + +impl Component for OnlineTableInner { + type Message = Msg; + + type Properties = OnlineTableInnerProps; - { - let ctx = ctx.clone(); - let update_table_state = update_table_state.clone(); - use_effect_with_deps( - move |_| { - ctx.spawn_async(async move { - update_table_state().await?; - Ok(()) - }); - || () - }, - (), + fn create(ctx: &yew::Context<Self>) -> Self { + ctx.link().send_future( + services::get_table_player_view(ctx.props().table.clone()) + .map(Msg::TableStateUpdated), ); + Self { table_state: None } } - let on_bid = { - let ctx = ctx.clone(); - let props = props.clone(); - let update_table_state = update_table_state.clone(); - Callback::from(move |bid| { - let update_table_state = update_table_state.clone(); - info!("Bid clicked: {:?}", bid); - ctx.spawn_async(async move { - let bid_response = Request::post(&format!( - "/api/table/{}/bid", - props.table.id - )) - .json(&bid)? - .send() - .await - .context("submitting bid")?; - let () = ok_json(bid_response).await?; - update_table_state().await?; - Ok(()) - }); - }) - }; + fn update(&mut self, ctx: &yew::Context<Self>, msg: Msg) -> bool { + match msg { + Msg::Bid(bid) => { + info!("Bid clicked: {bid:?}"); + let table = ctx.props().table.clone(); + ctx.link().send_future( + async move { + services::bid(table.clone(), bid).await?; + services::get_table_player_view(table).await + } + .map(Msg::TableStateUpdated), + ); + false + } + Msg::Play(card) => { + self.play(ctx, card); + false + } + Msg::TableStateUpdated(Ok(table_state)) => { + self.table_state = Some(table_state); + true + } + Msg::TableStateUpdated(Err(error)) => { + ctx.props().app_ctx.set_error(error); + false + } + } + } - let leave_table = { - let ctx = ctx.clone(); - Callback::from(move |_| { - ctx.leave_table(); - }) - }; + fn view(&self, ctx: &yew::Context<Self>) -> Html { + let table_state = match &self.table_state { + Some(x) => x, + None => return loading(), + }; - let center = match &*table_state { - Some(GameStatePlayerView::Bidding(bidding)) => - bidding_view(bidding, on_bid), - Some(GameStatePlayerView::Playing(playing)) => - playing_view(playing), - None => html! { <p>{"Loading table"}</p> }, - }; + let center = match table_state { + GameStatePlayerView::Bidding(bidding) => { + bidding_view(bidding, ctx.link().callback(Msg::Bid)) + } + GameStatePlayerView::Playing(playing) => { + playing_view(playing, ctx.link().callback(Msg::Play)) + } + }; - html! { - <> - <p>{ format!("This is table {}", props.table.id) }</p> - <button onclick={leave_table}> - { "Leave table" } - </button> - <div class="game-layout"> - { center } - </div> - </> + let leave_table = { + let ctx = ctx.props().app_ctx.clone(); + Callback::from(move |_| ctx.leave_table()) + }; + + html! { + <> + <div class="game-layout"> + {center} + </div> + <p>{format!("This is table {}", ctx.props().table.id)}</p> + <button onclick={leave_table}> + { "Leave table" } + </button> + </> + } } } -#[derive(PartialEq, Properties, Clone)] -pub struct OnlineTableProps { - pub table: protocol::Table, +fn loading() -> Html { + html! { <p>{"Loading table information"}</p> } } pub fn bidding_view( @@ -123,7 +155,7 @@ pub fn bidding_view( { format!("It is {:?} to bid", bidding.bidding.current_bidder()) } </div> <div class="hand south"> - <Hand cards={ bidding.hand.clone() } on_card_clicked={ Callback::from(|card| {}) } /> + <Hand cards={ bidding.hand.clone() } on_card_clicked={ Callback::from(|_| {}) } /> </div> <h2>{ "Table view" }</h2> <pre>{ format!("{:#?}", bidding) }</pre> @@ -132,16 +164,20 @@ pub fn bidding_view( } pub fn playing_view( - playing: &PlayStatePlayerView) - -> Html { - let on_card_clicked = Callback::from(|card| {}); - // Dummy is assumed to be north for now. + playing: &PlayStatePlayerView, + on_card_clicked: Callback<Card>, +) -> Html { + // Only one layout is currently supported. + assert_eq!(playing.player_position, Player::South); + assert_eq!(playing.contract.declarer, Player::South); + let dummy = match &playing.dummy { Some(hand) => html! { <Hand cards={hand.clone()} on_card_clicked={on_card_clicked.clone()}/> }, None => html! {<p>{"Dummy is not visible yet"}</p>}, }; + html! { <> <div class="center"> diff --git a/webapp/src/main.rs b/webapp/src/main.rs index 73531d5..de4dd7a 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -8,6 +8,7 @@ use components::{AppContextProvider, ErrorInfo, OnlineTable}; extern crate wee_alloc; pub mod routing; use crate::{components::use_app_context, routing::Route}; +pub mod services; // Use `wee_alloc` as the global allocator. #[global_allocator] @@ -55,8 +56,6 @@ fn home() -> Html { }) }; - info!("home(): Error is {:?}", ctx.error()); - html! { <ul> <li>{ user }</li> diff --git a/webapp/src/services.rs b/webapp/src/services.rs new file mode 100644 index 0000000..212363f --- /dev/null +++ b/webapp/src/services.rs @@ -0,0 +1,36 @@ +use anyhow::Context; +use gloo_net::http::Request; +use protocol::{bridge_engine::{GameStatePlayerView, Bid}, Table, card::Card}; + +use crate::utils::ok_json; + +pub async fn get_table_player_view( + table: Table, +) -> Result<GameStatePlayerView, anyhow::Error> { + let response = Request::get(&format!("/api/table/{}", table.id)) + .send() + .await + .context("fetching table data")?; + let table = ok_json(response).await?; + Ok(table) +} + +pub async fn bid(table: Table, bid: Bid) -> Result<(), anyhow::Error> { + let response = + Request::post(&format!("/api/table/{}/bid", table.id)) + .json(&bid)? + .send() + .await + .context("submitting bid")?; + ok_json(response).await +} + +pub async fn play(table: Table, card: Card) -> Result<(), anyhow::Error> { + let response = + Request::post(&format!("/api/table/{}/play", table.id)) + .json(&card)? + .send() + .await + .context("submitting play")?; + ok_json(response).await +} |