summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-12-23 11:29:37 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-12-23 11:30:01 -0500
commit6c9651194fda7a9167157e835fbe9fd691e9a1a9 (patch)
tree4e24f243bde8923f15b125db863ba58732617251
parent868703627bfd27925a53fcfbdd3dbeef831660c8 (diff)
Replace table with a struct component
This makes async & state handling much easier
-rw-r--r--Cargo.lock28
-rw-r--r--webapp/Cargo.toml1
-rw-r--r--webapp/src/components/app_context_provider.rs6
-rw-r--r--webapp/src/components/table.rs228
-rw-r--r--webapp/src/main.rs3
-rw-r--r--webapp/src/services.rs36
6 files changed, 204 insertions, 98 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1748d04..afac954 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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
+}