diff --git a/Cargo.lock b/Cargo.lock index c781bd9..26f9659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1570,8 +1570,6 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", - "openssl-sys", "url", ] @@ -2055,26 +2053,10 @@ checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", - "libssh2-sys", "libz-sys", - "openssl-sys", "pkg-config", ] -[[package]] -name = "libssh2-sys" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - [[package]] name = "libz-sys" version = "1.1.6" @@ -2225,15 +2207,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minidom" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e" -dependencies = [ - "quick-xml", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2802,15 +2775,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-xml" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.18" @@ -3200,42 +3164,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "seed" -version = "0.9.1" -source = "git+https://github.com/seed-rs/seed#689055ef4f466f28dca621c23f5e2df7438bd1bc" -dependencies = [ - "console_error_panic_hook", - "cookie", - "dbg", - "enclose", - "futures", - "getrandom", - "gloo-file", - "gloo-timers", - "gloo-utils", - "indexmap", - "js-sys", - "rand", - "serde", - "serde_json", - "uuid 0.8.2", - "version_check 0.9.4", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "seed_heroicons" -version = "0.1.0" -source = "git+https://github.com/mh84/seed_heroicons.git#32569ac5ba9adcffb168fbdaac8b258e99e73d86" -dependencies = [ - "git2", - "minidom", - "seed 0.9.1 (git+https://github.com/seed-rs/seed)", -] - [[package]] name = "semver" version = "0.9.0" @@ -4358,8 +4286,7 @@ dependencies = [ "js-sys", "model", "rusty-money", - "seed 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "seed_heroicons", + "seed", "serde", "serde-wasm-bindgen", "serde_json", diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index b8a8735..b2340ae 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -7,6 +7,7 @@ mod dummy; pub mod encrypt; use std::fmt::{Display, Formatter}; +use std::ops; use std::str::FromStr; use derive_more::{Deref, DerefMut, Display, From}; @@ -276,6 +277,14 @@ impl Default for Audience { #[serde(transparent)] pub struct Price(NonNegative); +impl ops::Mul for Price { + type Output = Self; + + fn mul(self, rhs: Quantity) -> Self::Output { + Self(NonNegative(rhs.0 .0 * self.0 .0)) + } +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] @@ -283,6 +292,22 @@ pub struct Price(NonNegative); #[serde(transparent)] pub struct Quantity(NonNegative); +impl ops::Add for Quantity { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl ops::Sub for Quantity { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + impl TryFrom for Quantity { type Error = TransformError; @@ -366,6 +391,22 @@ impl<'de> serde::Deserialize<'de> for Email { #[serde(transparent)] pub struct NonNegative(i32); +impl std::ops::Add for NonNegative { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl std::ops::Sub for NonNegative { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self((self.0 - rhs.0).max(0)) + } +} + impl TryFrom for NonNegative { type Error = TransformError; diff --git a/web/Cargo.toml b/web/Cargo.toml index a01b684..33ef72f 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib"] model = { path = "../shared/model", features = ["dummy"] } seed = { version = "0.9.1", features = [] } -seed_heroicons = { git = "https://github.com/mh84/seed_heroicons.git" } +#seed_heroicons = { git = "https://github.com/mh84/seed_heroicons.git" } chrono = { version = "*", features = ["wasm-bindgen", "wasmbind"] } gloo-timers = { version = "*", features = ["futures"] } diff --git a/web/src/i18n/pl.rs b/web/src/i18n/pl.rs index 3d7c34f..b5a899c 100644 --- a/web/src/i18n/pl.rs +++ b/web/src/i18n/pl.rs @@ -35,5 +35,10 @@ pub fn define(i18n: &mut I18n) { "There was internal server error. Please try later", "Wystąpił błąd, proszę spróbować później", ) - .define("Can't create account", "Adres e-mail i/lub login są zajęte"); + .define("Can't create account", "Adres e-mail i/lub login są zajęte") + .define("Checkout", "Zapłać") + .define("Home", "Home") + .define("Account", "Profil") + .define("Shopping cart", "Koszyk") + .define("Qty:", "Ilość:"); } diff --git a/web/src/lib.rs b/web/src/lib.rs index dce2c6c..e846c61 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -83,6 +83,7 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { shared: shared::Model::default(), i18n: I18n::load(), cart: Default::default(), + #[cfg(debug_assertions)] debug_modal: false, }; @@ -119,9 +120,9 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { let page = fetch_page!(public model, SignIn); pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into)) } - Msg::Public(pages::public::Msg::SignUp(pages::public::sign_up::Msg::AccountCreated( - res, - ))) => { + Msg::Public(pages::public::Msg::SignUp( + pages::public::sign_up::RegisterMsg::AccountCreated(res), + )) => { orders .skip() .send_msg(Msg::Session(SessionMsg::TokenRefreshed(res))); @@ -130,17 +131,21 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { let page = fetch_page!(public model, SignUp); pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into)) } + Msg::Public(pages::public::Msg::ShoppingCart(msg)) => { + let page = fetch_page!(public model, ShoppingCart); + pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into)) + } Msg::Admin(_) => {} Msg::Session(msg) => { session::update(msg, model, orders); } + Msg::Cart(msg) => { + shopping_cart::update(msg, model, orders); + } #[cfg(debug_assertions)] Msg::Debug(msg) => { debug::update(msg, model); } - Msg::Cart(msg) => { - shopping_cart::update(msg, model, orders); - } } } @@ -150,6 +155,9 @@ fn view(model: &Model) -> Node { Page::Public(PublicPage::Product(page)) => pages::public::product::view(model, page), Page::Public(PublicPage::SignIn(page)) => pages::public::sign_in::view(model, page), Page::Public(PublicPage::SignUp(page)) => pages::public::sign_up::view(model, page), + Page::Public(PublicPage::ShoppingCart(page)) => { + pages::public::shopping_cart::view(model, page) + } _ => empty![], }; diff --git a/web/src/model.rs b/web/src/model.rs index d775a58..1f52207 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -1,6 +1,8 @@ +use std::collections::{HashMap, HashSet}; + use seed::Url; -use crate::{I18n, Page, shopping_cart}; +use crate::{shopping_cart, I18n, Page}; #[derive(Debug)] pub struct Model { @@ -11,6 +13,51 @@ pub struct Model { pub shared: crate::shared::Model, pub i18n: I18n, pub cart: shopping_cart::ShoppingCart, + #[cfg(debug_assertions)] pub debug_modal: bool, } + +#[derive(Debug, Default)] +pub struct Products { + pub categories: Vec, + pub product_ids: Vec, + pub products: HashMap, +} + +impl Products { + pub fn update(&mut self, products: Vec) { + let len = products.len(); + self.categories = products + .iter() + .fold(HashSet::with_capacity(len), |mut set, p| { + if let Some(category) = p.category.as_ref().cloned() { + set.insert(category); + } + set + }) + .into_iter() + .collect(); + self.categories.sort_by(|a, b| a.name.cmp(&b.name)); + self.product_ids = products.iter().map(|p| p.id).collect(); + self.products = { + let len = products.len(); + products + .into_iter() + .fold(HashMap::with_capacity(len), |mut m, p| { + m.insert(p.id, p); + m + }) + }; + } + + pub fn filter_product_ids(&self, filter: F) -> Vec + where + F: Fn(&model::api::Product) -> bool, + { + self.product_ids + .iter() + .filter_map(|id| self.products.get(id).filter(|&p| filter(p)).map(|p| p.id)) + .collect() + } +} diff --git a/web/src/pages.rs b/web/src/pages.rs index 6c9b92e..aa18272 100644 --- a/web/src/pages.rs +++ b/web/src/pages.rs @@ -34,7 +34,7 @@ pub enum PublicPage { Product(public::product::ProductPage), SignIn(public::sign_in::SignInPage), SignUp(public::sign_up::SignUpPage), - ShoppingCart, + ShoppingCart(public::shopping_cart::ShoppingCartPage), Checkout, } @@ -59,6 +59,9 @@ impl Page { url, &mut orders.proxy(Into::into), ))), + ["shopping-cart", _rest @ ..] => Self::Public(PublicPage::ShoppingCart( + public::shopping_cart::init(url, &mut orders.proxy(Into::into)), + )), ["sign-in", _rest @ ..] => Self::Public(PublicPage::SignIn(public::sign_in::init( url, &mut orders.proxy(Into::into), @@ -89,6 +92,11 @@ impl Page { let page = crate::fetch_page!(public page self, Product, Page::init(url, orders)); public::product::page_changed(url, page); } + ["shopping-cart", _rest @ ..] => { + let page = + crate::fetch_page!(public page self, ShoppingCart, Page::init(url, orders)); + public::shopping_cart::page_changed(url, page); + } ["sign-in", ..] => { let page = crate::fetch_page!(public page self, SignIn, Page::init(url, orders)); public::sign_in::page_changed(url, page); @@ -131,6 +139,10 @@ impl<'a> Urls<'a> { self.base_url().add_path_part("sign-in") } + pub fn profile(self) -> Url { + self.base_url().add_path_part("profile") + } + pub fn sign_up(self) -> Url { self.base_url().add_path_part("sign-up") } diff --git a/web/src/pages/public.rs b/web/src/pages/public.rs index 9f01e8b..22864d0 100644 --- a/web/src/pages/public.rs +++ b/web/src/pages/public.rs @@ -1,64 +1,78 @@ pub mod listing; pub mod product; +pub mod shopping_cart; pub mod sign_in; -pub(crate) mod sign_up; +pub mod sign_up; #[derive(Debug)] pub enum Msg { - Listing(listing::Msg), - Product(product::Msg), - SignIn(sign_in::Msg), - SignUp(sign_up::Msg), + Listing(listing::ListingMsg), + Product(product::ProductMsg), + SignIn(sign_in::LogInMsg), + SignUp(sign_up::RegisterMsg), + ShoppingCart(shopping_cart::ShoppingCartMsg), } -impl From for Msg { - fn from(msg: listing::Msg) -> Self { +impl From for Msg { + fn from(msg: listing::ListingMsg) -> Self { Self::Listing(msg) } } -impl From for Msg { - fn from(msg: product::Msg) -> Self { +impl From for Msg { + fn from(msg: product::ProductMsg) -> Self { Self::Product(msg) } } -impl From for Msg { - fn from(msg: sign_in::Msg) -> Self { +impl From for Msg { + fn from(msg: sign_in::LogInMsg) -> Self { Self::SignIn(msg) } } -impl From for Msg { - fn from(msg: sign_up::Msg) -> Self { +impl From for Msg { + fn from(msg: sign_up::RegisterMsg) -> Self { Self::SignUp(msg) } } -impl From for crate::Msg { - fn from(msg: listing::Msg) -> Self { +impl From for Msg { + fn from(msg: shopping_cart::ShoppingCartMsg) -> Self { + Self::ShoppingCart(msg) + } +} + +impl From for crate::Msg { + fn from(msg: listing::ListingMsg) -> Self { crate::Msg::Public(msg.into()) } } -impl From for crate::Msg { - fn from(msg: product::Msg) -> Self { +impl From for crate::Msg { + fn from(msg: product::ProductMsg) -> Self { crate::Msg::Public(msg.into()) } } -impl From for crate::Msg { - fn from(msg: sign_in::Msg) -> Self { +impl From for crate::Msg { + fn from(msg: sign_in::LogInMsg) -> Self { crate::Msg::Public(msg.into()) } } -impl From for crate::Msg { - fn from(msg: sign_up::Msg) -> Self { +impl From for crate::Msg { + fn from(msg: sign_up::RegisterMsg) -> Self { crate::Msg::Public(msg.into()) } } +impl From for crate::Msg { + fn from(msg: shopping_cart::ShoppingCartMsg) -> Self { + Self::Public(msg.into()) + } +} + impl From for crate::Msg { fn from(msg: Msg) -> Self { crate::Msg::Public(msg) diff --git a/web/src/pages/public/listing.rs b/web/src/pages/public/listing.rs index ab2f9eb..0b0dec8 100644 --- a/web/src/pages/public/listing.rs +++ b/web/src/pages/public/listing.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use model::Quantity; use seed::app::Orders; @@ -6,33 +6,31 @@ use seed::prelude::*; use seed::*; use crate::api::NetRes; +use crate::model::Products; use crate::pages::Urls; use crate::shopping_cart::CartMsg; #[derive(Debug)] pub struct ListingPage { - pub product_ids: Vec, - pub products: HashMap, pub errors: Vec, - pub categories: Vec, pub filters: HashSet, pub visible_products: Vec, + + pub products: Products, } #[derive(Debug)] -pub enum Msg { +pub enum ListingMsg { FetchProducts, - ProductFetched(NetRes), + ProductsFetched(NetRes), } -pub fn init(url: Url, orders: &mut impl Orders) -> ListingPage { - orders.send_msg(Msg::FetchProducts); +pub fn init(url: Url, orders: &mut impl Orders) -> ListingPage { + orders.send_msg(ListingMsg::FetchProducts); ListingPage { - product_ids: vec![], + products: Products::default(), filters: url_to_filters(url), - products: Default::default(), errors: vec![], - categories: vec![], visible_products: vec![], } } @@ -53,62 +51,41 @@ fn url_to_filters(mut url: Url) -> HashSet { pub fn page_changed(url: Url, model: &mut ListingPage) { model.filters = url_to_filters(url); - filter_products(model) + let ids = { + model + .products + .filter_product_ids(|product| filter_product(&*model, product)) + }; + model.visible_products = ids; } -fn filter_products(model: &mut ListingPage) { - model.visible_products = model - .product_ids - .iter() - .filter_map(|id| { - model.products.get(id).and_then(|p| { - p.category - .as_ref() - .filter(|c| model.filters.contains(c.key.as_str())) - .map(|_| p.id) - }) - }) - .collect(); +fn filter_product(model: &ListingPage, product: &model::api::Product) -> bool { + product + .category + .as_ref() + .filter(|c| model.filters.contains(c.key.as_str())) + .is_some() } -pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders) { +pub fn update(msg: ListingMsg, model: &mut ListingPage, orders: &mut impl Orders) { match msg { - Msg::FetchProducts => { + ListingMsg::FetchProducts => { orders.skip().perform_cmd({ async { crate::Msg::Public( - Msg::ProductFetched(crate::api::public::fetch_products().await).into(), + ListingMsg::ProductsFetched(crate::api::public::fetch_products().await).into(), ) } }); } - Msg::ProductFetched(NetRes::Success(products)) => { - model.categories = products - .0 - .iter() - .fold(HashSet::new(), |mut set, p| { - if let Some(category) = p.category.as_ref().cloned() { - set.insert(category); - } - set - }) - .into_iter() - .collect(); - model.categories.sort_by(|a, b| a.name.cmp(&b.name)); - model.product_ids = products.0.iter().map(|p| p.id).collect(); - model.products = { - let len = products.0.len(); - products - .0 - .into_iter() - .fold(HashMap::with_capacity(len), |mut m, p| { - m.insert(p.id, p); - m - }) - }; - filter_products(model); + ListingMsg::ProductsFetched(NetRes::Success(products)) => { + model.products.update(products.0); + let ids = model + .products + .filter_product_ids(|product| filter_product(model, product)); + model.visible_products = ids; } - Msg::ProductFetched(NetRes::Error(_)) | Msg::ProductFetched(NetRes::Http(_)) => { + ListingMsg::ProductsFetched(NetRes::Error(_)) | ListingMsg::ProductsFetched(NetRes::Http(_)) => { model.errors.push("Failed to load products".into()); } } @@ -116,15 +93,16 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders Node { let products: Vec> = if page.visible_products.is_empty() { - page.product_ids + page.products + .product_ids .iter() - .filter_map(|id| page.products.get(id)) + .filter_map(|id| page.products.products.get(id)) .map(|p| product(model, p)) .collect() } else { page.visible_products .iter() - .filter_map(|id| page.products.get(id)) + .filter_map(|id| page.products.products.get(id)) .map(|p| product(model, p)) .collect() }; @@ -136,8 +114,8 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node { .map_msg(Into::into); div![ - crate::shared::view::public_navbar(model), - super::layout::view(model, content, Some(&page.categories)) + crate::shared::view::public_navbar::view(model, &page.products), + super::layout::view(model, content, Some(&page.products.categories)) ] } diff --git a/web/src/pages/public/product.rs b/web/src/pages/public/product.rs index 5400b7a..7088bf4 100644 --- a/web/src/pages/public/product.rs +++ b/web/src/pages/public/product.rs @@ -4,54 +4,60 @@ use seed::*; use crate::api::NetRes; #[derive(Debug)] -pub enum Msg { - ProductFetched(NetRes), +pub enum ProductMsg { + ProductsFetched(NetRes), SelectImage(usize), } #[derive(Debug, Default)] pub struct ProductPage { pub product_id: Option, - pub product: Option, + pub products: crate::model::Products, pub selected_image: usize, } -pub fn init(mut url: Url, orders: &mut impl Orders) -> ProductPage { +pub fn init(mut url: Url, orders: &mut impl Orders) -> ProductPage { let product_id = match url.remaining_path_parts().as_slice() { ["product", id] => id.parse::().unwrap_or_default().into(), _ => return ProductPage::default(), }; orders.perform_cmd(async move { - Msg::ProductFetched(crate::api::public::fetch_product(product_id).await) + crate::Msg::from(ProductMsg::ProductsFetched( + crate::api::public::fetch_products().await, + )) }); ProductPage { product_id: Some(product_id), - product: None, + products: Default::default(), selected_image: 0, } } pub fn page_changed(_url: Url, _model: &mut ProductPage) {} -pub fn update(msg: Msg, model: &mut ProductPage, _orders: &mut impl Orders) { +pub fn update(msg: ProductMsg, model: &mut ProductPage, _orders: &mut impl Orders) { match msg { - Msg::ProductFetched(NetRes::Success(product)) => { - model.product = Some(product); + ProductMsg::ProductsFetched(NetRes::Success(products)) => { + model.products.update(products.0); } - Msg::ProductFetched(NetRes::Error(e)) => { + ProductMsg::ProductsFetched(NetRes::Error(e)) => { seed::error!(e); } - Msg::ProductFetched(NetRes::Http(e)) => { + ProductMsg::ProductsFetched(NetRes::Http(e)) => { seed::error!(e); } - Msg::SelectImage(selected) => { + ProductMsg::SelectImage(selected) => { model.selected_image = selected; } } } pub fn view(model: &crate::Model, page: &ProductPage) -> Node { - let product = match page.product.as_ref() { + let product_id = match &page.product_id { + Some(id) => id, + _ => return empty![], + }; + let product = match page.products.products.get(product_id) { None => return empty!(), Some(product) => product, }; @@ -115,12 +121,12 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node { ].map_msg(Into::into); div![ - crate::shared::view::public_navbar(model), - super::layout::view(model, content, None) + crate::shared::view::public_navbar::view(model, &page.products), + super::layout::view(model, content, Some(&page.products.categories)) ] } -fn action_section(product: &model::api::Product, model: &crate::Model) -> Node { +fn action_section(product: &model::api::Product, model: &crate::Model) -> Node { if product.available { div![ C!["flex py-4 space-x-4"], @@ -139,7 +145,7 @@ fn action_section(product: &model::api::Product, model: &crate::Model) -> Node + None as Option }) ] ] @@ -148,7 +154,7 @@ fn action_section(product: &model::api::Product, model: &crate::Model) -> Node Node { +fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node { match product.deliver_days_flag.len() { 0 => return empty![], 7 => return div![model.i18n.t("Delivery all week")], @@ -167,7 +173,7 @@ fn delivery_available(product: &model::api::Product, model: &crate::Model) -> No ] } -fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node { +fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node { div![ C!["flex-1 px-2"], button![ @@ -180,13 +186,13 @@ fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node Node { +fn image(img: &model::api::Photo) -> Node { div![ C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4 flex items-center justify-center"], img![C!["h-64 md:h-80"], attrs!["src" => img.url.as_str()]] diff --git a/web/src/pages/public/shopping_cart.rs b/web/src/pages/public/shopping_cart.rs new file mode 100644 index 0000000..eadf2b2 --- /dev/null +++ b/web/src/pages/public/shopping_cart.rs @@ -0,0 +1,177 @@ +use rusty_money::Money; +use seed::prelude::*; +use seed::*; + +use crate::api::NetRes; + +#[derive(Debug)] +pub enum ShoppingCartMsg { + ProductsFetched(NetRes), +} + +#[derive(Debug)] +pub struct ShoppingCartPage { + pub products: crate::model::Products, +} + +pub fn init(_url: Url, orders: &mut impl Orders) -> ShoppingCartPage { + orders.perform_cmd(async move { + crate::Msg::from(ShoppingCartMsg::ProductsFetched( + crate::api::public::fetch_products().await, + )) + }); + ShoppingCartPage { + products: Default::default(), + } +} + +pub fn page_changed(_url: Url, _model: &mut ShoppingCartPage) {} + +pub fn update( + msg: ShoppingCartMsg, + model: &mut ShoppingCartPage, + _orders: &mut impl Orders, +) { + match msg { + ShoppingCartMsg::ProductsFetched(NetRes::Success(products)) => { + model.products.update(products.0); + } + ShoppingCartMsg::ProductsFetched(NetRes::Error(e)) => { + seed::error!(e); + } + ShoppingCartMsg::ProductsFetched(NetRes::Http(e)) => { + seed::error!(e); + } + } +} + +pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node { + div![ + C!["flex justify-center my-6"], + div![ + C!["flex flex-col w-full p-8 text-gray-800 bg-white shadow-lg pin-r pin-y md:w-4/5 lg:w-4/5"], + products(model, page) + ] + ] +} + +fn products(model: &crate::Model, page: &ShoppingCartPage) -> Node { + div![ + C!["flex-1"], + table![ + C!["w-full text-sm lg:text-base"], + attrs!["cellspacing" => 0], + products_head(model), + products_body(model, page), + ] + ] +} + +fn products_head(model: &crate::Model) -> Node { + thead![tr![ + C!["h-12 uppercase"], + th![C!["hidden md:table-cell"]], + th![C!["text-left"], model.i18n.t("Product")], + th![ + C!["lg:text-right text-left pl-5 lg:pl-0"], + span![ + C!["lg:hidden"], + attrs![At::Title => model.i18n.t("Quantity")], + model.i18n.t("Qtd") + ], + span![C!["hidden lg:inline"], model.i18n.t("Quantity")] + ], + th![ + C!["hidden text-right md:table-cell"], + model.i18n.t("Unit price") + ], + th![C!["text-right"], model.i18n.t("Total price")] + ]] +} + +fn products_body(model: &crate::Model, page: &ShoppingCartPage) -> Node { + let items = model + .cart + .items + .values() + .filter_map(|item: &crate::shopping_cart::Item| { + page.products + .products + .get(&item.product_id) + .map(|product| (item, product)) + }) + .map( + |(item, product): (&crate::shopping_cart::Item, &model::api::Product)| { + item_view(model, item, product) + }, + ); + tbody![items] +} + +fn item_view( + model: &crate::Model, + item: &crate::shopping_cart::Item, + product: &model::api::Product, +) -> Node { + use rusty_money::iso::PLN; + use rusty_money::Money; + + let img = product + .photos + .first() + .map(|photo| photo.url.as_str()) + .unwrap_or_default(); + + tr![ + td![ + C!["hidden pb-4 md:table-cell"], + a![ + attrs![At::Href => "#"], + img![attrs![ + At::Src => img, + At::Class => "w-20 rounded", + At::Alt => "Thumbnail" + ]], + ] + ], + td![a![ + attrs![At::Href => "#"], + p![C!["mb-2 md:ml-4"], product.name.as_str()], + form![ + attrs![ "action" => "", "method" => "POST"], + button![ + attrs!["type" => "submit", "class" => "text-gray-700 md:ml-4"], + small![model.i18n.t("(Remove item)]")] + ] + ] + ]], + td![ + C!["justify-center md:justify-end md:flex mt-6"], + div![ + C!["w-20 h-10"], + div![ + C!["relative flex flex-row w-full h-8"], + input![attrs![ + At::Type => "number", + At::Value => **item.quantity, + At::Class => "w-full font-semibold text-center text-gray-700 bg-gray-200 outline-none focus:outline-none hover:text-black focus:text-black" + ]], + ] + ] + ], + td![ + C!["hidden text-right md:table-cell"], + span![ + C!["text-sm lg:text-base font-medium"], + Money::from_minor(**product.price as i64, PLN).to_string() + ] + ], + td![ + C!["text-right"], + span![ + C!["text-sm lg:text-base font-medium"], + Money::from_minor(**(product.price * item.quantity) as i64, PLN).to_string() + ] + ] + ] +} diff --git a/web/src/pages/public/sign_in.rs b/web/src/pages/public/sign_in.rs index 0cc452f..ac576af 100644 --- a/web/src/pages/public/sign_in.rs +++ b/web/src/pages/public/sign_in.rs @@ -4,7 +4,7 @@ use seed::*; use crate::pages::Urls; #[derive(Debug)] -pub enum Msg { +pub enum LogInMsg { LoginChanged(String), PasswordChanged(String), Submit, @@ -16,7 +16,7 @@ pub struct SignInPage { pub password: model::Password, } -pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignInPage { +pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignInPage { SignInPage { login: model::Login::new(""), password: model::Password::new(""), @@ -25,7 +25,7 @@ pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignInPage { pub fn page_changed(_url: Url, _model: &mut SignInPage) {} -pub fn update(_msg: Msg, _model: &mut SignInPage, _orders: &mut impl Orders) {} +pub fn update(_msg: LogInMsg, _model: &mut SignInPage, _orders: &mut impl Orders) {} pub fn view(model: &crate::Model, page: &SignInPage) -> Node { let home = Urls::new(&model.url).home(); @@ -66,13 +66,13 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node { div![super::layout::view(model, content, None)] } -fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node { +fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node { form![ C!["mt-6"], ev("submit", |ev| { ev.stop_propagation(); ev.prevent_default(); - Msg::Submit + LogInMsg::Submit }), div![ label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")], @@ -82,7 +82,7 @@ fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node { ev(Ev::Change, |ev| { ev.stop_propagation(); ev.prevent_default(); - ev.target().map(|target| seed::to_input(&target).value()).map(Msg::LoginChanged) + ev.target().map(|target| seed::to_input(&target).value()).map(LogInMsg::LoginChanged) }) ] ], @@ -94,7 +94,7 @@ fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node { ev(Ev::Change, |ev| { ev.stop_propagation(); ev.prevent_default(); - ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordChanged) + ev.target().map(|target| seed::to_input(&target).value()).map(LogInMsg::PasswordChanged) }) ], a![ diff --git a/web/src/pages/public/sign_up.rs b/web/src/pages/public/sign_up.rs index fa7d863..87cf44d 100644 --- a/web/src/pages/public/sign_up.rs +++ b/web/src/pages/public/sign_up.rs @@ -7,7 +7,7 @@ use seed::*; use crate::pages::Urls; #[derive(Debug)] -pub enum Msg { +pub enum RegisterMsg { LoginChanged(String), EmailChanged(String), PasswordChanged(String), @@ -24,7 +24,7 @@ pub struct SignUpPage { pub password_confirmation: model::PasswordConfirmation, } -pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignUpPage { +pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignUpPage { SignUpPage { login: model::Login::new(""), email: model::Email::invalid_empty(), @@ -35,23 +35,23 @@ pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignUpPage { pub fn page_changed(_url: Url, _model: &mut SignUpPage) {} -pub fn update(msg: Msg, model: &mut SignUpPage, orders: &mut impl Orders) { +pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders) { match msg { - Msg::LoginChanged(value) => { + RegisterMsg::LoginChanged(value) => { model.login = Login::new(value); } - Msg::EmailChanged(value) => { + RegisterMsg::EmailChanged(value) => { if let Ok(email) = Email::from_str(&value) { model.email = email; } } - Msg::PasswordChanged(value) => { + RegisterMsg::PasswordChanged(value) => { model.password = Password::new(value); } - Msg::PasswordConfirmationChanged(value) => { + RegisterMsg::PasswordConfirmationChanged(value) => { model.password_confirmation = PasswordConfirmation::new(value); } - Msg::Submit => { + RegisterMsg::Submit => { let email = model.email.clone(); let login = model.login.clone(); let password = model.password.clone(); @@ -59,7 +59,7 @@ pub fn update(msg: Msg, model: &mut SignUpPage, orders: &mut impl Orders {} + RegisterMsg::AccountCreated(_) => {} } } @@ -114,13 +114,13 @@ pub fn view(model: &crate::Model, page: &SignUpPage) -> Node { div![super::layout::view(model, content, None)] } -fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { +fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { form![ C!["mt-6"], ev("submit", |ev| { ev.stop_propagation(); ev.prevent_default(); - Msg::Submit + RegisterMsg::Submit }), div![ label![attrs!["for" => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")], @@ -129,7 +129,7 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"], ev(Ev::Change, |ev| { ev.stop_propagation(); - ev.target().map(|target| seed::to_input(&target).value()).map(Msg::EmailChanged) + ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::EmailChanged) }) ] ], @@ -140,7 +140,7 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"], ev(Ev::Change, |ev| { ev.stop_propagation(); - ev.target().map(|target| seed::to_input(&target).value()).map(Msg::LoginChanged) + ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::LoginChanged) }) ] ], @@ -151,7 +151,7 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { input![attrs!["type" => "password", "id" => "password"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]], ev(Ev::Change, |ev| { ev.stop_propagation(); - ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordChanged) + ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordChanged) }) ], div![ @@ -166,7 +166,7 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { ], ev(Ev::Change, |ev| { ev.stop_propagation(); - ev.target().map(|target| seed::to_input(&target).value()).map(Msg::PasswordConfirmationChanged) + ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordConfirmationChanged) }) ], div![ diff --git a/web/src/session.rs b/web/src/session.rs index f7d518b..84e5de3 100644 --- a/web/src/session.rs +++ b/web/src/session.rs @@ -4,7 +4,7 @@ use seed::prelude::*; use crate::pages::AdminPage; use crate::shared::notification::NotificationMsg; -use crate::{pages, Model, Msg, Page, PublicPage}; +use crate::{Model, Msg, Page, PublicPage}; #[derive(Debug)] pub enum SessionMsg { @@ -60,7 +60,7 @@ pub fn redirect_on_session(model: &Model, orders: &mut impl Orders) { PublicPage::Product(_) => {} PublicPage::SignIn(_) => {} PublicPage::SignUp(_) => {} - PublicPage::ShoppingCart => {} + PublicPage::ShoppingCart(_) => {} PublicPage::Checkout => {} }, } diff --git a/web/src/shared.rs b/web/src/shared.rs index a2a8f97..77e7dc8 100644 --- a/web/src/shared.rs +++ b/web/src/shared.rs @@ -1,6 +1,8 @@ use seed::app::Orders; +use crate::api::NetRes; use crate::session::redirect_on_session; +use crate::shared::notification::NotificationMsg; pub mod notification; pub mod view; @@ -8,10 +10,16 @@ pub mod view; #[derive(Debug)] pub enum SharedMsg { LoadMe, - MeLoaded(crate::api::NetRes), + MeLoaded(NetRes), SignIn(model::api::SignInInput), - SignedIn(crate::api::NetRes), - Notification(notification::NotificationMsg), + SignedIn(NetRes), + Notification(NotificationMsg), +} + +impl From for crate::Msg { + fn from(msg: SharedMsg) -> Self { + Self::Shared(msg) + } } #[derive(Debug, Default)] @@ -38,22 +46,22 @@ pub fn update(msg: SharedMsg, model: &mut crate::Model, orders: &mut impl Orders }); } } - SharedMsg::MeLoaded(crate::api::NetRes::Success(account)) => { + SharedMsg::MeLoaded(NetRes::Success(account)) => { model.shared.me = Some(account); redirect_on_session(model, orders); } - SharedMsg::MeLoaded(crate::api::NetRes::Error(_error)) => {} - SharedMsg::MeLoaded(crate::api::NetRes::Http(_error)) => {} + SharedMsg::MeLoaded(NetRes::Error(_error)) => {} + SharedMsg::MeLoaded(NetRes::Http(_error)) => {} SharedMsg::SignIn(input) => { orders.skip().perform_cmd(async { SharedMsg::SignedIn(crate::api::public::sign_in(input).await) }); } - SharedMsg::SignedIn(crate::api::NetRes::Success(pair)) => { + SharedMsg::SignedIn(NetRes::Success(pair)) => { handle_auth_pair(pair, &mut model.shared, orders); } - SharedMsg::SignedIn(crate::api::NetRes::Error(_err)) => {} - SharedMsg::SignedIn(crate::api::NetRes::Http(_err)) => {} + SharedMsg::SignedIn(NetRes::Error(_err)) => {} + SharedMsg::SignedIn(NetRes::Http(_err)) => {} SharedMsg::Notification(msg) => { notification::update(msg, &mut model.shared, orders); } diff --git a/web/src/shared/view.rs b/web/src/shared/view.rs index a8854f0..8d50973 100644 --- a/web/src/shared/view.rs +++ b/web/src/shared/view.rs @@ -1,78 +1,233 @@ -use seed::prelude::*; -use seed::*; +pub mod public_navbar { + use seed::prelude::*; + use seed::*; -use crate::pages::Urls; -use crate::Msg; + use crate::pages::Urls; + use crate::shopping_cart::CartMsg; + use crate::Msg; -pub fn public_navbar(model: &crate::Model) -> Node { - header![ - C!["container flex justify-around py-8 mx-auto bg-white"], - div![C!["flex items-center"], logo(model)], - div![ - C!["items-center hidden space-x-8 lg:flex"], - navbar_item(div![C![""], "Home"], Urls::new(model.url.clone()).home()), - ], - div![ - C!["flex items-center space-x-2"], - navbar_item(account(), Urls::new(model.url.clone()).sign_in()), - navbar_item(bag(), Urls::new(model.url.clone()).shopping_cart()) - ] - ] -} - -fn navbar_item(name: Node, path: Url) -> Node { - a![ - attrs!["href" => path], - C!["px-4 py-2 font-semibold text-gray-600 rounded"], - name - ] -} - -fn logo(model: &crate::Model) -> Node { - a![ - attrs!["href" => "/"], - match model.logo.as_deref() { - Some(url) => img![ - C!["text-2xl font-extrabold text-blue-600"], - attrs!["alt" => "logo", "src" => url, "height" => "32", "style" => "height: 64px;"] + pub fn view(model: &crate::Model, products: &crate::model::Products) -> Node { + header![ + C!["container flex justify-around py-8 mx-auto bg-white"], + div![C!["flex items-center"], logo(model)], + div![ + C!["items-center hidden space-x-8 lg:flex"], + navbar_item(div![C![""], "Home"], Urls::new(&model.url).home()), ], - _ => span![C!["text-2xl font-extrabold text-blue-600"], "logo"], - } - ] + div![ + C!["flex items-center space-x-2"], + navbar_item( + div![ + attrs![At::Title => model.i18n.t("Account")], + account() + ], + model.shared.me.as_ref() + .map(|_| Urls::new(&model.url).profile()) + .unwrap_or_else(|| Urls::new(&model.url).sign_in()) + ), + navbar_item( + div![ + C!["relative"], + attrs![At::Title => model.i18n.t("Shopping cart")], + bag(), + span![ + C!["absolute bottom-0 right-0 w-4 h-4 bg-gray-500 text-xs text-white text-center rounded-full ring ring-white"], + model + .cart + .items + .values() + .map(|item| **item.quantity) + .sum::() + ], + super::cart_dropdown::view(model, products), + ev(Ev::MouseEnter, |_ev| Msg::from(CartMsg::Hover)), + ev(Ev::MouseLeave, |_ev| Msg::from(CartMsg::Leave)), + ], + Urls::new(model.url.clone()).shopping_cart() + ) + ] + ] + } + + fn navbar_item(name: Node, path: Url) -> Node { + a![ + attrs!["href" => path], + C!["px-4 py-2 font-semibold text-gray-600 rounded"], + name + ] + } + + fn logo(model: &crate::Model) -> Node { + a![ + attrs![At::Href => Urls::new(&model.url).home()], + match model.logo.as_deref() { + Some(url) => img![ + C!["text-2xl font-extrabold text-blue-600"], + attrs!["alt" => "logo", "src" => url, "height" => "32", "style" => "height: 64px;"] + ], + _ => span![C!["text-2xl font-extrabold text-blue-600"], "logo"], + } + ] + } + + fn bag() -> Node { + svg![ + attrs![ + "width" => "32px", + "height" => "32px", + At::ViewBox => "0 0 32 32", + "xmlns" => "http://www.w3.org/2000/svg", + At::Class => "w-6 h-6", + At::Fill => "none", + At::Stroke => "currentColor", + At::StrokeLinecap => "round", + At::StrokeLineJoin => "round", + At::StrokeWidth => "2" + ], + path![attrs![At::D => "M5 9 L5 29 27 29 27 9 Z M10 9 C10 9 10 3 16 3 22 3 22 9 22 9"]] + ] + } + + fn account() -> Node { + svg![ + attrs![ + "xmlns" => "http://www.w3.org/2000/svg", + At::Class => "w-6 h-6", + At::Fill => "none", + At::ViewBox => "0 0 24 24", + At::Stroke => "currentColor", + ], + path![attrs![ + At::StrokeLinecap => "round", + At::StrokeLineJoin => "round", + At::StrokeWidth => "2", + At::D => "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z", + ]], + ] + } } -fn bag() -> Node { - svg![ - attrs![ - "width" => "32px", - "height" => "32px", - "viewBox" => "0 0 32 32", - "xmlns" => "http://www.w3.org/2000/svg", - "class"=>"w-6 h-6", - "fill" => "none", - "stroke" => "currentColor", - "stroke-linecap" => "round", - "stroke-linejoin" => "round", - "stroke-width" => "2" - ], - path![attrs!["d" => "M5 9 L5 29 27 29 27 9 Z M10 9 C10 9 10 3 16 3 22 3 22 9 22 9"]] - ] -} +pub mod cart_dropdown { + use model::ProductId; + use seed::prelude::*; + use seed::*; -fn account() -> Node { - svg![ - attrs![ - "xmlns" => "http://www.w3.org/2000/svg", - "class" => "w-6 h-6", - "fill" => "none", - "viewBox" => "0 0 24 24", - "stroke" => "currentColor", - ], - path![attrs![ - "stroke-linecap" => "round", - "stroke-linejoin" => "round", - "stroke-width" => "2", - "d" => "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ]], - ] + use crate::shopping_cart::Item; + use crate::{Model, Msg}; + + macro_rules! filter_products { + ($model: expr, $products: expr) => { + $model + .cart + .items + .values() + .filter_map(|item: &Item| filter_product(item, $products, item.product_id)) + }; + } + + pub fn view(model: &Model, products: &crate::model::Products) -> Node { + let items = filter_products!(model, products).map(|(it, product)| item(model, it, product)); + div![ + C![ + "absolute w-full rounded-b border-t-0 z-10", + if model.cart.hover { + "visible" + } else { + "invisible" + } + ], + div![C!["shadow-xl w-64"], items, checkout(model, products)] + ] + } + + fn item(model: &Model, item: &Item, product: &model::api::Product) -> Node { + let img = product + .photos + .first() + .map(|photo| photo.url.as_str()) + .unwrap_or_default(); + + let price = rusty_money::Money::from_minor( + **(product.price * item.quantity) as i64, + rusty_money::iso::PLN, + ) + .to_string(); + div![ + C!["p-2 flex bg-white hover:bg-gray-100 cursor-pointer border-b border-gray-100"], + div![C!["p-2 w-12"], img![attrs![At::Src => img]]], + div![ + C!["flex-auto text-sm w-32"], + div![C!["font-bold"], product.name.as_str()], + div![C!["truncate"], product.short_description.as_str()], + div![ + C!["text-gray-400"], + model.i18n.t("Qty:"), + " ", + **item.quantity + ], + ], + div![ + C!["flex flex-col w-18 font-medium items-end"], + div![ + C!["w-4 h-4 mb-6 hover:bg-red-200 rounded-full cursor-pointer text-red-700"], + trash_icon(), + ], + price + ] + ] + } + + fn trash_icon() -> Node { + svg![ + attrs![ + "xmlns" => Namespace::Svg.as_str(), + "width" => "100%", + "height" => "100%", + "fill" => "none", + "viewBox" => "0 0 24 24", + "stroke" => "currentColor", + "stroke-width" => "2", + "stroke-linecap" => "round", + "stroke-linejoin" => "round", + "class" => "feather feather-trash-2" + ], + polyline![attrs!["points" => "3 6 5 6 21 6"]], + path![ + attrs!["d"=>"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"] + ], + line_![attrs!["x1"=>"10", "y1" => "11", "x2" => "10", "y2" => "17"]], + line_![attrs!["x1"=>"14", "y1" => "11", "x2" => "14", "y2" => "17"]] + ] + } + + fn checkout(model: &Model, products: &crate::model::Products) -> Node { + let sum: i32 = filter_products!(model, products) + .map(|(item, product): (&Item, &model::api::Product)| **item.quantity * **product.price) + .sum(); + let sum = rusty_money::Money::from_minor(sum as i64, rusty_money::iso::PLN); + + div![ + C!["p-4 justify-center flex"], + button![ + C![ + "text-base undefined hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer" + "hover:bg-teal-700 hover:text-teal-100 bg-white text-teal-700 border duration-200 ease-in-out border-teal-600 transition" + ], + model.i18n.t("Checkout"), + " ", + format!("{}", sum) + ] + ] + } + + fn filter_product<'item, 'product>( + item: &'item Item, + products: &'product crate::model::Products, + product_id: ProductId, + ) -> Option<(&'item Item, &'product model::api::Product)> { + products + .products + .get(&product_id) + .map(|product| (item, product)) + } } diff --git a/web/src/shopping_cart.rs b/web/src/shopping_cart.rs index 0f7f04e..33eec8e 100644 --- a/web/src/shopping_cart.rs +++ b/web/src/shopping_cart.rs @@ -16,6 +16,8 @@ pub enum CartMsg { quantity_unit: QuantityUnit, product_id: ProductId, }, + Hover, + Leave, } impl From for Msg { @@ -28,15 +30,17 @@ pub type Items = indexmap::IndexMap; #[derive(Debug, Serialize, Deserialize)] pub struct Item { - product_id: ProductId, - quantity: Quantity, - quantity_unit: QuantityUnit, + pub product_id: ProductId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct ShoppingCart { pub cart_id: Option, pub items: Items, + #[serde(skip)] + pub hover: bool, } pub fn init(model: &mut Model, _orders: &mut impl Orders) { @@ -52,22 +56,34 @@ pub fn update(msg: CartMsg, model: &mut Model, _orders: &mut impl Orders) { } => { { let items: &mut Items = &mut model.cart.items; - let entry = items.entry(product_id).or_insert_with(|| Item { + let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item { quantity, quantity_unit, product_id, }); - entry.quantity = quantity; + entry.quantity = entry.quantity + quantity; entry.quantity_unit = quantity_unit; } store_local(&model.cart); } CartMsg::ModifyItem { .. } => {} + CartMsg::Hover => { + model.cart.hover = true; + } + CartMsg::Leave => { + model.cart.hover = false; + } } } fn load_local() -> ShoppingCart { - LocalStorage::get("ct").unwrap_or_default() + match LocalStorage::get("ct") { + Ok(cart) => cart, + Err(e) => { + seed::error!(e); + ShoppingCart::default() + } + } } fn store_local(cart: &ShoppingCart) { LocalStorage::insert("ct", cart).ok();