diff --git a/.gitignore b/.gitignore index d796d4c..0beb66b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ +.DS_Store /target Cargo.lock +static/*.wasm +static/*.d.ts +static/wasm.js +static/package.json http://www.sheshbabu.com/posts/rust-wasm-yew-single-page-application/ diff --git a/Cargo.toml b/Cargo.toml index 2b4bc43..be49d2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib","rlib"] [dependencies] yew = "0.17" +yew-router = "0.14.0" wasm-bindgen = "0.2" anyhow = "1.0.32" serde = { version = "1.0", features = ["derive"] } diff --git a/Makefile.toml b/Makefile.toml index 62528aa..1371114 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,7 +1,7 @@ [tasks.build] command = "wasm-pack" args = ["build", "--dev", "--target", "web", "--out-name", "wasm", "--out-dir", "./static"] -watch = {ignore_pattern = "static/*"} +watch = { ignore_pattern = "static/*" } [tasks.serve] command = "simple-http-server" diff --git a/src/api.rs b/src/api.rs index 64600ad..0e68e68 100644 --- a/src/api.rs +++ b/src/api.rs @@ -14,3 +14,11 @@ pub fn get_products(callback: FetchCallback>) -> FetchTask { FetchService::fetch(req, callback).unwrap() } + +pub fn get_product(id: i32, callback: FetchCallback) -> FetchTask { + let req = Request::get(format!("/products/{}.json", id)) + .body(Nothing) + .unwrap(); + + FetchService::fetch(req, callback).unwrap() +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..c97851e --- /dev/null +++ b/src/app.rs @@ -0,0 +1,83 @@ +use crate::components::Navbar; +use crate::types::{CartProduct, Product}; +use yew::prelude::*; +use yew_router::prelude::*; + +use crate::pages::{Home, ProductDetail}; +use crate::route::Route; + +struct State { + cart_products: Vec, +} + +pub struct App { + state: State, + link: ComponentLink, +} + +pub enum Msg { + AddToCart(Product), +} + +impl Component for App { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + let cart_products = vec![]; + + Self { + state: State { cart_products }, + link, + } + } + + fn update(&mut self, message: Self::Message) -> ShouldRender { + match message { + Msg::AddToCart(product) => { + let cart_product = self + .state + .cart_products + .iter_mut() + .find(|cp: &&mut CartProduct| cp.product.id == product.id); + + if let Some(cp) = cart_product { + cp.quantity += 1; + } else { + self.state.cart_products.push(CartProduct { + product: product.clone(), + quantity: 1, + }) + } + true + } + } + } + + fn change(&mut self, _: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + let handle_add_to_cart = self + .link + .callback(|product: Product| Msg::AddToCart(product)); + let cart_products = self.state.cart_products.clone(); + + let render = Router::render(move |switch: Route| match switch { + Route::ProductDetail(id) => { + html! {} + } + Route::HomePage => { + html! {} + } + }); + + html! { + <> + + render=render/> + + } + } +} diff --git a/src/components/atc_button.rs b/src/components/atc_button.rs new file mode 100644 index 0000000..1fb9172 --- /dev/null +++ b/src/components/atc_button.rs @@ -0,0 +1,46 @@ +use crate::types::Product; +use yew::prelude::*; + +pub struct AtcButton { + props: Props, + link: ComponentLink, +} + +#[derive(Properties, Clone)] +pub struct Props { + pub product: Product, + pub on_add_to_cart: Callback, +} + +pub enum Msg { + AddToCart, +} + +impl Component for AtcButton { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + Self { props, link } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::AddToCart => self.props.on_add_to_cart.emit(self.props.product.clone()), + } + true + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props = props; + true + } + + fn view(&self) -> Html { + let onclick = self.link.callback(|_| Msg::AddToCart); + + html! { + + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index bcbd853..d1b2868 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,7 @@ +mod atc_button; +mod navbar; mod product_card; +pub use atc_button::AtcButton; +pub use navbar::Navbar; pub use product_card::ProductCard; diff --git a/src/components/navbar.rs b/src/components/navbar.rs new file mode 100644 index 0000000..d17e99c --- /dev/null +++ b/src/components/navbar.rs @@ -0,0 +1,44 @@ +use crate::types::CartProduct; +use yew::prelude::*; + +pub struct Navbar { + props: Props, +} + +#[derive(Properties, Clone)] +pub struct Props { + pub cart_products: Vec, +} + +impl Component for Navbar { + type Message = (); + type Properties = Props; + + fn create(props: Self::Properties, _link: ComponentLink) -> Self { + Self { props } + } + + fn update(&mut self, _msg: Self::Message) -> ShouldRender { + true + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props = props; + true + } + + fn view(&self) -> Html { + let cart_value = self + .props + .cart_products + .iter() + .fold(0.0, |acc, cp| acc + (cp.quantity as f64 * cp.product.price)); + + html! { + + } + } +} diff --git a/src/components/product_card.rs b/src/components/product_card.rs index d139014..9124a51 100644 --- a/src/components/product_card.rs +++ b/src/components/product_card.rs @@ -1,5 +1,8 @@ +use crate::components::AtcButton; +use crate::route::Route; use crate::types::Product; use yew::prelude::*; +use yew_router::components::RouterAnchor; pub struct ProductCard { props: Props, @@ -8,7 +11,7 @@ pub struct ProductCard { #[derive(Properties, Clone)] pub struct Props { pub product: Product, - pub on_add_to_cart: Callback<()>, + pub on_add_to_cart: Callback, } impl Component for ProductCard { @@ -28,15 +31,17 @@ impl Component for ProductCard { } fn view(&self) -> Html { - let onclick = self.props.on_add_to_cart.reform(|_| ()); + type Anchor = RouterAnchor; html! { -
- -
{&self.props.product.name}
-
{"$"}{&self.props.product.price}
- -
+
+ + +
{&self.props.product.name}
+
{"$"}{&self.props.product.price}
+
+ +
} } } diff --git a/src/lib.rs b/src/lib.rs index f74a494..b56a898 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,14 @@ mod api; +mod app; mod components; -mod types; mod pages; +mod route; +mod types; -use pages::Home; use wasm_bindgen::prelude::*; use yew::prelude::*; #[wasm_bindgen(start)] pub fn run_app() { - App::::new().mount_to_body(); + App::::new().mount_to_body(); } diff --git a/src/pages/home.rs b/src/pages/home.rs index ecee725..296583e 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -3,24 +3,29 @@ use crate::components::ProductCard; use crate::types::{CartProduct, Product}; use anyhow::Error; use yew::format::Json; -use yew::services::fetch::FetchTask; use yew::prelude::*; +use yew::services::fetch::FetchTask; struct State { products: Vec, - cart_products: Vec, - get_product_error: Option, + get_products_error: Option, get_products_loaded: bool, } +#[derive(Properties, Clone)] +pub struct Props { + pub cart_products: Vec, + pub on_add_to_cart: Callback, +} + pub struct Home { - state: State, - link: ComponentLink, - task: Option, + props: Props, + state: State, + link: ComponentLink, + task: Option, } pub enum Msg { - AddToCart(i32), GetProducts, GetProductsSuccess(Vec), GetProductsError(Error), @@ -28,24 +33,23 @@ pub enum Msg { impl Component for Home { type Message = Msg; - type Properties = (); + type Properties = Props; - fn create(_: Self::Properties, link: ComponentLink) -> Self { + fn create(props: Self::Properties, link: ComponentLink) -> Self { let products = vec![]; - let cart_products = vec![]; link.send_message(Msg::GetProducts); Self { - state: State { - products, - cart_products, - get_product_error: None, - get_products_loaded: false, - }, - link, - task: None, - } + props, + state: State { + products, + get_products_error: None, + get_products_loaded: false, + }, + link, + task: None, + } } fn update(&mut self, message: Self::Message) -> ShouldRender { @@ -61,6 +65,7 @@ impl Component for Home { Err(err) => Msg::GetProductsError(err), } }); + self.task = Some(api::get_products(handler)); true } @@ -70,37 +75,15 @@ impl Component for Home { true } Msg::GetProductsError(error) => { - self.state.get_product_error = Some(error); + self.state.get_products_error = Some(error); self.state.get_products_loaded = true; true } - Msg::AddToCart(product_id) => { - let product = self - .state - .products - .iter() - .find(|p: &&Product| p.id == product_id) - .unwrap(); - let cart_product = self - .state - .cart_products - .iter_mut() - .find(|cp: &&mut CartProduct| cp.product.id == product_id); - - if let Some(cp) = cart_product { - cp.quantity += 1; - } else { - self.state.cart_products.push(CartProduct { - product: product.clone(), - quantity: 1, - }) - } - true - } } } - fn change(&mut self, _: Self::Properties) -> ShouldRender { + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props = props; true } @@ -110,35 +93,28 @@ impl Component for Home { .products .iter() .map(|product: &Product| { - let product_id = product.id; html! { - + } }) .collect(); - let cart_value = self - .state - .cart_products - .iter() - .fold(0.0, |acc, cp| acc + (cp.quantity as f64 * cp.product.price)); - if !self.state.get_products_loaded { html! { -
{"Loading ..."}
+
+
+
{"Loading ..."}
+
} - } else if let Some(_) = self.state.get_product_error { + } else if let Some(_) = self.state.get_products_error { html! { -
- {"Error loading products! :("} -
+
+ {"Error loading products! :("} +
} } else { html! { -
- {format!("Cart Value: {:.2}", cart_value)} - {products} -
+
{products}
} } } diff --git a/src/pages/mod.rs b/src/pages/mod.rs index f4fa64a..1bd2830 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,3 +1,5 @@ mod home; +mod product_detail; pub use home::Home; +pub use product_detail::ProductDetail; diff --git a/src/pages/product_detail.rs b/src/pages/product_detail.rs new file mode 100644 index 0000000..e5edad7 --- /dev/null +++ b/src/pages/product_detail.rs @@ -0,0 +1,112 @@ +use crate::api; +use crate::components::AtcButton; +use crate::types::Product; +use anyhow::Error; +use yew::format::Json; +use yew::prelude::*; +use yew::services::fetch::FetchTask; + +struct State { + product: Option, + get_product_error: Option, + get_product_loaded: bool, +} + +pub struct ProductDetail { + props: Props, + state: State, + link: ComponentLink, + task: Option, +} + +#[derive(Properties, Clone)] +pub struct Props { + pub id: i32, + pub on_add_to_cart: Callback, +} + +pub enum Msg { + GetProduct, + GetProductSuccess(Product), + GetProductError(Error), +} + +impl Component for ProductDetail { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + link.send_message(Msg::GetProduct); + + Self { + props, + state: State { + product: None, + get_product_error: None, + get_product_loaded: false, + }, + link, + task: None, + } + } + + fn update(&mut self, message: Self::Message) -> ShouldRender { + match message { + Msg::GetProduct => { + let handler = self + .link + .callback(move |response: api::FetchResponse| { + let (_, Json(data)) = response.into_parts(); + match data { + Ok(product) => Msg::GetProductSuccess(product), + Err(err) => Msg::GetProductError(err), + } + }); + + self.task = Some(api::get_product(self.props.id, handler)); + true + } + Msg::GetProductSuccess(product) => { + self.state.product = Some(product); + self.state.get_product_loaded = true; + true + } + Msg::GetProductError(error) => { + self.state.get_product_error = Some(error); + self.state.get_product_loaded = true; + true + } + } + } + + fn change(&mut self, _: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + if let Some(ref product) = self.state.product { + html! { +
+ +
{&product.name}
+
{&product.description}
+
{"$"}{&product.price}
+ +
+ } + } else if !self.state.get_product_loaded { + html! { +
+
+
{"Loading ..."}
+
+ } + } else { + html! { +
+ {"Error loading product! :("} +
+ } + } + } +} diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 0000000..159d97a --- /dev/null +++ b/src/route.rs @@ -0,0 +1,9 @@ +use yew_router::prelude::*; + +#[derive(Switch, Debug, Clone)] +pub enum Route { + #[to = "/product/{id}"] + ProductDetail(i32), + #[to = "/"] + HomePage, +} diff --git a/src/types.rs b/src/types.rs index b9d4f8a..9202a87 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,15 +2,15 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Product { - pub id: i32, - pub name: String, - pub description: String, - pub image: String, - pub price: f64, + pub id: i32, + pub name: String, + pub description: String, + pub image: String, + pub price: f64, } #[derive(Clone, Debug)] pub struct CartProduct { - pub product: Product, - pub quantity: i32, + pub product: Product, + pub quantity: i32, } diff --git a/static/index.html b/static/index.html index 975f0a5..27332df 100644 --- a/static/index.html +++ b/static/index.html @@ -1,9 +1,13 @@ - - + RustSPA + +
diff --git a/static/wasm.js b/static/wasm.js index fab4ab6..ec912aa 100644 --- a/static/wasm.js +++ b/static/wasm.js @@ -624,12 +624,12 @@ async function init(input) { imports.wbg.__wbindgen_throw = function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }; - imports.wbg.__wbindgen_closure_wrapper3480 = logError(function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_25); + imports.wbg.__wbindgen_closure_wrapper5049 = logError(function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 212, __wbg_adapter_22); return addHeapObject(ret); }); - imports.wbg.__wbindgen_closure_wrapper4695 = logError(function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_22); + imports.wbg.__wbindgen_closure_wrapper3744 = logError(function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 153, __wbg_adapter_25); return addHeapObject(ret); }); diff --git a/static/wasm_bg.wasm b/static/wasm_bg.wasm index e3d5829..816089c 100644 Binary files a/static/wasm_bg.wasm and b/static/wasm_bg.wasm differ