diff --git a/actors/cart_manager/src/lib.rs b/actors/cart_manager/src/lib.rs index d3f71cc..c79ac33 100644 --- a/actors/cart_manager/src/lib.rs +++ b/actors/cart_manager/src/lib.rs @@ -100,45 +100,38 @@ pub struct AddItem { cart_async_handler!(AddItem, add_item, ShoppingCartItem); async fn add_item(msg: AddItem, db: actix::Addr) -> Result { - match db - .send(database_manager::EnsureActiveShoppingCart { + let _cart = query_db!( + db, + database_manager::EnsureActiveShoppingCart { buyer_id: msg.buyer_id, - }) - .await - { - Ok(Ok(_)) => {} - _ => return Err(Error::ShoppingCartFailed), - }; - let cart = match db - .send(database_manager::AccountShoppingCarts { + }, + Error::ShoppingCartFailed + ); + let mut carts: Vec = query_db!( + db, + database_manager::AccountShoppingCarts { account_id: msg.buyer_id, state: Some(ShoppingCartState::Active), - }) - .await - .map(|res| match res { - Ok(mut v) if !v.is_empty() => Ok(v.remove(0)), - Err(e) => Err(Error::Db(e)), - _ => Err(Error::CartNotAvailable), - }) { - Ok(Ok(cart)) => cart, - Ok(Err(e)) => { - log::error!("{e:?}"); - return Err(e); - } - _ => return Err(Error::CartNotAvailable), + }, + passthrough Error::Db, + Error::CartNotAvailable + ); + let cart = if carts.is_empty() { + return Err(Error::CartNotAvailable); + } else { + carts.remove(0) }; - match db - .send(database_manager::CreateShoppingCartItem { + Ok(query_db!( + db, + database_manager::CreateShoppingCartItem { product_id: msg.product_id, shopping_cart_id: cart.id, quantity: msg.quantity, quantity_unit: msg.quantity_unit, - }) - .await - { - Ok(res) => res.map_err(Into::into), - _ => Err(Error::CantAddItem), - } + }, + passthrough Error::Db, + Error::CantAddItem + )) } #[derive(actix::Message)] diff --git a/api/src/routes/public.rs b/api/src/routes/public.rs index 596c083..f3b016c 100644 --- a/api/src/routes/public.rs +++ b/api/src/routes/public.rs @@ -9,7 +9,7 @@ use model::api::Config; #[macro_export] macro_rules! public_send_db { ($db: expr, $msg: expr) => {{ - use crate::routes::PublicError; + use $crate::routes::PublicError; return match $db.send($msg).await { Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)), @@ -59,8 +59,8 @@ async fn landing() -> HttpResponse { HttpResponse::NotImplemented().body("") } -#[get("/config.json")] -async fn client_config(config: Data) -> Json { +#[get("/config")] +async fn client_config(config: Data) -> Json { let (optional_payment, currency) = { let lock = config.lock(); let p = lock.payment(); @@ -76,6 +76,8 @@ async fn client_config(config: Data) -> Json vec![model::PaymentMethod::PayU], }, currency, + shipping: false, + shipping_methods: vec![], }) } @@ -113,8 +115,8 @@ async fn svg(path: Path) -> HttpResponse { pub fn configure(config: &mut ServiceConfig) { config + .service(client_config) .service(landing) .service(svg) - .service(client_config) .configure(api_v1::configure); } diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index 4402721..cd014e5 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -17,7 +17,7 @@ pub struct Config { pub coupons: bool, pub currency: String, pub shipping: bool, - pub shipping_methods: Vec, + pub shipping_methods: Vec, } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index f4e6717..3084a22 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -591,7 +591,7 @@ where { fn decode( value: >::ValueRef, - ) -> Result> { + ) -> Result> { let value = >::decode(value)?; Ok(Days( (0..7) @@ -1099,10 +1099,12 @@ pub struct ProductPhoto { } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[cfg_attr(feature = "db", derive(sqlx::Type))] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum ShippingMethod { + /// Build-in InPost shipping InPost, - Custom(String), + /// Shop owner will ship product manually + Manual, } diff --git a/web/Trunk.toml b/web/Trunk.toml index 09d7f22..133af1c 100644 --- a/web/Trunk.toml +++ b/web/Trunk.toml @@ -12,3 +12,7 @@ backend = "http://localhost:8080/files" [[proxy]] rewrite = "/svg" backend = "http://localhost:8080/svg" + +[[proxy]] +rewrite = "/config" +backend = "http://localhost:8080/config" diff --git a/web/src/api/public.rs b/web/src/api/public.rs index 14fe26d..bd2bebc 100644 --- a/web/src/api/public.rs +++ b/web/src/api/public.rs @@ -4,7 +4,7 @@ use seed::fetch::{Header, Method, Request}; use crate::api::perform; pub async fn config() -> super::NetRes { - perform(Request::new("/config.json").method(Method::Get)).await + perform(Request::new("/config").method(Method::Get)).await } pub async fn fetch_products() -> super::NetRes { diff --git a/web/src/lib.rs b/web/src/lib.rs index 1fc6e5a..eeb4cd7 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -157,33 +157,38 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { shared::update(msg, model, orders); } Msg::UrlChanged(subs::UrlChanged(url)) => model.page.page_changed(url, orders), - Msg::Public(pages::public::Msg::Listing(msg)) => { + Msg::Public(pages::public::PublicMsg::Listing(msg)) => { let page = fetch_page!(public model, Listing); pages::public::listing::update(msg, page, &mut orders.proxy(Into::into)); } - Msg::Public(pages::public::Msg::Product(msg)) => { + Msg::Public(pages::public::PublicMsg::Product(msg)) => { let page = fetch_page!(public model, Product); pages::public::product::update(msg, page, &mut orders.proxy(Into::into)) } - Msg::Public(pages::public::Msg::SignIn(msg)) => { + Msg::Public(pages::public::PublicMsg::SignIn(msg)) => { 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( + Msg::Public(pages::public::PublicMsg::SignUp( pages::public::sign_up::RegisterMsg::AccountCreated(res), )) => { orders .skip() .send_msg(Msg::Session(SessionMsg::TokenRefreshed(res))); } - Msg::Public(pages::public::Msg::SignUp(msg)) => { + Msg::Public(pages::public::PublicMsg::SignUp(msg)) => { 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)) => { + Msg::Public(pages::public::PublicMsg::ShoppingCart(msg)) => { let page = fetch_page!(public model, ShoppingCart); pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into)) } + Msg::Public(pages::public::PublicMsg::Checkout(msg)) => { + let page = fetch_page!(public model, Checkout); + pages::public::checkout::update(msg, page, &mut orders.proxy(Into::into)) + } + // Admin Msg::Admin(pages::admin::Msg::Landing(msg)) => { let page = fetch_page!(admin model, Landing); pages::admin::landing::update(msg, page, &mut orders.proxy(Into::into)) @@ -210,6 +215,7 @@ fn view(model: &Model) -> Node { Page::Public(PublicPage::ShoppingCart(page)) => { pages::public::shopping_cart::view(model, page) } + Page::Public(PublicPage::Checkout(page)) => pages::public::checkout::view(model, page), Page::Admin(AdminPage::Landing(page)) => pages::admin::landing::view(model, page), _ => empty![], }; diff --git a/web/src/model.rs b/web/src/model.rs index ad36c21..ac3f55c 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -24,6 +24,8 @@ pub struct Config { pub pay_methods: Vec<::model::PaymentMethod>, pub coupons: bool, pub currency: &'static rusty_money::iso::Currency, + pub shipping: bool, + pub shipping_methods: Vec, } impl Default for Config { @@ -32,6 +34,8 @@ impl Default for Config { pay_methods: vec![], coupons: false, currency: rusty_money::iso::PLN, + shipping: false, + shipping_methods: vec![], } } } @@ -42,12 +46,16 @@ impl From<::model::api::Config> for Config { pay_methods, coupons, currency, + shipping, + shipping_methods, }: model::api::Config, ) -> Self { Self { pay_methods, coupons, currency: rusty_money::iso::find(¤cy).unwrap_or(rusty_money::iso::PLN), + shipping, + shipping_methods, } } } diff --git a/web/src/pages.rs b/web/src/pages.rs index 8c11040..abca261 100644 --- a/web/src/pages.rs +++ b/web/src/pages.rs @@ -6,12 +6,40 @@ use seed::{struct_urls, Url}; use crate::{shared, NetRes}; +/// Implement msg conversion for +/// +/// Examples +/// +/// ``` +/// use crate::pages::*; +/// use crate::pages::public::*; +/// use crate::pages::public::checkout::*; +/// +/// impl_into_msg!(ShoppingCartMsg > PublicMsg as ShoppingCart > Public); +/// ``` +#[macro_export] +macro_rules! impl_into_msg { + ($msg: ty > $scoped_msg: ty as $scope_name: ident > $scope: ident) => { + impl From<$msg> for $crate::Msg { + fn from(msg: $msg) -> $crate::Msg { + $crate::Msg::$scope(msg.into()) + } + } + + impl From<$msg> for $scoped_msg { + fn from(msg: $msg) -> $scoped_msg { + <$scoped_msg>::$scope_name(msg) + } + } + }; +} + #[derive(Debug)] pub enum Msg { #[cfg(debug_assertions)] NoOp, Config(NetRes<::model::api::Config>), - Public(public::Msg), + Public(public::PublicMsg), Admin(admin::Msg), UrlChanged(subs::UrlChanged), Shared(shared::SharedMsg), @@ -36,7 +64,7 @@ pub enum PublicPage { SignIn(public::sign_in::SignInPage), SignUp(public::sign_up::SignUpPage), ShoppingCart(public::shopping_cart::ShoppingCartPage), - Checkout, + Checkout(public::checkout::CheckoutPage), } #[derive(Debug)] @@ -63,6 +91,10 @@ impl Page { ["shopping-cart", _rest @ ..] => Self::Public(PublicPage::ShoppingCart( public::shopping_cart::init(url, &mut orders.proxy(Into::into)), )), + ["checkout", _rest @ ..] => Self::Public(PublicPage::Checkout(public::checkout::init( + url, + &mut orders.proxy(Into::into), + ))), ["sign-in", _rest @ ..] => Self::Public(PublicPage::SignIn(public::sign_in::init( url, &mut orders.proxy(Into::into), @@ -101,6 +133,10 @@ impl Page { crate::fetch_page!(public page self, ShoppingCart, Page::init(url, orders)); public::shopping_cart::page_changed(url, page); } + ["checkout", _rest @ ..] => { + let page = crate::fetch_page!(public page self, Checkout, Page::init(url, orders)); + public::checkout::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); diff --git a/web/src/pages/public.rs b/web/src/pages/public.rs index cf3a360..1fd5611 100644 --- a/web/src/pages/public.rs +++ b/web/src/pages/public.rs @@ -1,3 +1,5 @@ +use crate::impl_into_msg; + pub mod checkout; pub mod listing; pub mod product; @@ -6,79 +8,21 @@ pub mod sign_in; pub mod sign_up; #[derive(Debug)] -pub enum Msg { +pub enum PublicMsg { Listing(listing::ListingMsg), Product(product::ProductMsg), SignIn(sign_in::LogInMsg), SignUp(sign_up::RegisterMsg), ShoppingCart(shopping_cart::ShoppingCartMsg), + Checkout(checkout::CheckoutMsg), } -impl From for Msg { - fn from(msg: listing::ListingMsg) -> Self { - Self::Listing(msg) - } -} - -impl From for Msg { - fn from(msg: product::ProductMsg) -> Self { - Self::Product(msg) - } -} - -impl From for Msg { - fn from(msg: sign_in::LogInMsg) -> Self { - Self::SignIn(msg) - } -} - -impl From for Msg { - fn from(msg: sign_up::RegisterMsg) -> Self { - Self::SignUp(msg) - } -} - -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::ProductMsg) -> Self { - crate::Msg::Public(msg.into()) - } -} - -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::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) - } -} +impl_into_msg!(listing::ListingMsg > PublicMsg as Listing > Public); +impl_into_msg!(product::ProductMsg > PublicMsg as Product > Public); +impl_into_msg!(sign_in::LogInMsg > PublicMsg as SignIn > Public); +impl_into_msg!(sign_up::RegisterMsg > PublicMsg as SignUp > Public); +impl_into_msg!(shopping_cart::ShoppingCartMsg > PublicMsg as ShoppingCart > Public); +impl_into_msg!(checkout::CheckoutMsg > PublicMsg as Checkout > Public); pub mod layout { use seed::prelude::*; diff --git a/web/src/pages/public/checkout.rs b/web/src/pages/public/checkout.rs index e69de29..904c2bb 100644 --- a/web/src/pages/public/checkout.rs +++ b/web/src/pages/public/checkout.rs @@ -0,0 +1,414 @@ +use seed::prelude::*; +use seed::*; + +use crate::model::Products; +use crate::NetRes; + +#[derive(Debug)] +pub enum CheckoutMsg { + ProductsFetched(NetRes), +} + +#[derive(Debug)] +pub struct CheckoutPage { + pub products: Products, +} + +pub fn init(_url: Url, orders: &mut impl Orders) -> CheckoutPage { + orders.perform_cmd(async move { + crate::Msg::from(CheckoutMsg::ProductsFetched( + crate::api::public::fetch_products().await, + )) + }); + CheckoutPage { + products: Default::default(), + } +} + +pub fn page_changed(_url: Url, _model: &mut CheckoutPage) {} + +pub fn update(msg: CheckoutMsg, model: &mut CheckoutPage, _orders: &mut impl Orders) { + match msg { + CheckoutMsg::ProductsFetched(NetRes::Success(products)) => { + model.products.update(products.0); + } + CheckoutMsg::ProductsFetched(NetRes::Error(e)) => { + seed::error!("fetch product error", e); + } + CheckoutMsg::ProductsFetched(NetRes::Http(e)) => { + seed::error!("fetch product http", e); + } + } +} + +pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node { + let content = 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"], + div![C!["-mx-3 md:flex items-start"], left_side::view(model, page), right_side::view(model, page)] + ] + ]; + + div![ + crate::shared::view::public_navbar::view(model, &page.products), + super::layout::view(model, content, None) + ] +} + +mod left_side { + use rusty_money::Money; + use seed::prelude::*; + use seed::*; + + use crate::pages::public::checkout::CheckoutPage; + use crate::shopping_cart::Item; + use crate::Msg; + + pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node { + div![ + C!["px-3 md:w-7/12 lg:pr-10"], + products(model, page), + total(model, page) + ] + } + + fn products(model: &crate::Model, page: &CheckoutPage) -> Node { + let products = model + .cart + .items + .values() + .filter_map(|item: &Item| { + page.products + .products + .get(&item.product_id) + .map(|product| (item, product)) + }) + .map(|(item, product)| product_view(model, product, item)); + div![ + C!["w-full mx-auto text-gray-800 font-light mb-6 border-b border-gray-200 pb-6"], + products + ] + } + + fn product_view(model: &crate::Model, product: &model::api::Product, item: &Item) -> Node { + let img = product + .photos + .first() + .as_ref() + .map(|photo| photo.url.as_str()) + .unwrap_or_default(); + let price = Money::from_minor( + **(product.price * item.quantity) as i64, + model.config.currency, + ) + .to_string(); + let split = if price.contains(',') { ',' } else { '.' }; + let mut price_parts = price.split(split); + + div![ + C!["w-full flex items-center"], + div![ + C!["overflow-hidden rounded-lg w-16 h-16 bg-gray-50 border border-gray-200"], + img![attrs![At::Src => img]] + ], + div![ + C!["flex-grow pl-3"], + h6![ + C!["font-semibold uppercase text-gray-600"], + product.name.as_str() + ], + p![C!["text-gray-400"], "x ", **item.quantity] + ], + div![ + span![ + C!["font-semibold text-gray-600 text-xl"], + price_parts.next() + ], + span![ + C!["font-semibold text-gray-600 text-sm"], + split.to_string(), + price_parts.next().unwrap_or("00") + ], + ] + ] + } + + fn total(model: &crate::Model, page: &CheckoutPage) -> Node { + let subtotal_value = 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)| { + **(product.price * item.quantity) + }, + ) + .sum::() as i64; + + // TODO: Shipping + let shipping_cost = 0; + + let total = subtotal_value + shipping_cost; + + div![ + C!["mb-6 pb-6 border-b border-gray-200 md:border-none text-gray-800 text-xl"], + div![ + C!["w-full flex items-center"], + div![ + C!["flex-grow"], + span![C!["text-gray-600"], model.i18n.t("Total")] + ], + div![ + C!["pl-3"], + span![ + C!["font-semibold"], + Money::from_minor(total, model.config.currency).to_string() + ] + ] + ] + ] + } +} + +mod right_side { + use model::PaymentMethod; + use seed::prelude::*; + use seed::*; + + use crate::pages::public::checkout::CheckoutPage; + use crate::Msg; + + static SELECTED_PAYMENT_METHOD: &str = "payment-selected-method"; + + pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node { + let pay_methods = model.config.pay_methods.iter().map(|ty| match ty { + PaymentMethod::PayU => pay_u(model), + PaymentMethod::PaymentOnTheSpot => pay_on_spot(model), + }); + div![ + C!["px-3 md:w-5/12"], + contact(model, page), + div![ + C!["w-full mx-auto rounded-lg bg-white border border-gray-200 text-gray-800 font-light mb-6"], + pay_methods + ], + pay_now(model) + ] + } + + fn contact(model: &crate::Model, _page: &CheckoutPage) -> Node { + if model.shared.me.is_some() { + // TODO: Display user addresses + return empty![]; + } + div![ + C!["w-full mx-auto rounded-lg bg-white border border-gray-200 p-3 text-gray-800 font-light mb-6"], + form![ + div![ + C!["mb-3"], + div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Contact")], + ], + div![ + C!["mb-3"], + div![ + input![ + C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"], + attrs![At::Id => "client-name", At::Placeholder => model.i18n.t("Name")] + ] + ], + ], + div![ + C!["mb-3"], + div![ + input![ + C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"], + attrs![At::Id => "client-email", At::Type => "email", At::Placeholder => model.i18n.t("E-Mail")] + ] + ], + ], + div![ + C!["mb-3"], + div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Address")], + ], + div![ + C!["mb-3"], + input![ + C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"], + attrs![At::Id => "client-street", At::Placeholder => model.i18n.t("Street")] + ] + ], + div![ + C!["mb-3"], + input![ + C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"], + attrs![At::Id => "client-city", At::Placeholder => model.i18n.t("City")] + ] + ], + div![ + C!["mb-3 inline-block w-1/2 pr-1"], + input![ + C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"], + attrs![At::Id => "client-country", At::Placeholder => model.i18n.t("Country")] + ] + ], + div![ + C!["mb-3 inline-block -mx-1 pl-1 w-1/2"], + input![ + C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"], + attrs![At::Id => "client-zip", At::Placeholder => model.i18n.t("Zip")] + ] + ], + ] + ] + } + + fn pay_now(model: &crate::Model) -> Node { + div![ + button![ + C!["w-full max-w-xs mx-auto bg-indigo-500 hover:bg-indigo-700 focus:bg-indigo-700 text-white rounded-lg px-3 py-2 font-semibold text-center flex justify-center"], + padlock_icon(), + model.i18n.t("PAY NOW"), + ] + ] + } + + fn padlock_icon() -> Node { + svg![ + attrs![At::Xmlns => "http://www.w3.org/2000/svg", At::ViewBox => "0 0 32 32"], + C!["fill-white h-6 mr-3"], + path![ + attrs![At::D => "M25 13H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V15a2 2 0 0 0-2-2zM7 28V15h18v13zm2-17a1 1 0 0 0 1-1V8a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2a1 1 0 0 0 2 0V8a6 6 0 0 0-6-6h-4a6 6 0 0 0-6 6v2a1 1 0 0 0 1 1z"] + ], + path![attrs![At::D => "M16 18a1 1 0 0 0-1 1v6a1 1 0 0 0 2 0v-6a1 1 0 0 0-1-1Z"]], + ] + } + + fn pay_u(model: &crate::Model) -> Node { + div![ + C!["w-full p-3 border-b border-gray-200"], + label![ + C!["flex items-center cursor-pointer"], + attrs![At::For => "pay_u"], + input![ + C!["form-radio h-5 w-5 text-indigo-500"], + attrs![At::Id => "pay_u", At::Name => SELECTED_PAYMENT_METHOD, At::Type => "radio", At::Value => "pay_u"], + ], + span![ + C!["flex items-center"], + pay_u_icon(), + span![C!["ml-3"], model.i18n.t("PayU")] + ] + ], + ] + } + + fn pay_u_icon() -> Node { + svg![ + C!["ml-3"], + attrs![ + At::Xmlns => "http://www.w3.org/2000/svg", + At::ViewBox => "0 0 120 76", + At::Style => "height: 21px; width: 33px;", + ], + g![ + attrs![ + At::Fill => "none", + At::FillRule => "evenodd" + ], + path![attrs![ + At::Fill => "#1AAF5D", + At::D => "M111.999 0H8C3.582 0 0 3.59 0 8.008v59.984C0 72.415 3.591 76 8.001 76H112c4.419 0 8.001-3.59 8.001-8.008V8.008C120 3.585 116.409 0 111.999 0Z" + ]], + path![attrs![ + At::D => "M89.794 27.902H88.57a2.243 2.243 0 0 0-2.242 2.243V44.22s-.116 2.447-4.181 2.447c-4.066 0-4.181-2.447-4.181-2.447V30.145a2.243 2.243 0 0 0-2.242-2.243h-2.451a2.243 2.243 0 0 0-2.242 2.242V43.61c0 4.841 3.927 8.768 8.77 8.768h4.691a8.77 8.77 0 0 0 8.77-8.768v-10.2h2.653a1.02 1.02 0 0 0 1.019-1.019v-5.1a1.02 1.02 0 0 0-1.019-1.02h-5.1a1.02 1.02 0 0 0-1.02 1.02v.612zm0 0h1.226a2.244 2.244 0 0 1 2.242 2.242v3.265h-2.449a1.02 1.02 0 0 1-1.019-1.019v-4.488zm6.935-6.118c0-.45.365-.817.817-.817h3.67c.45 0 .816.365.816.817v3.67a.816.816 0 0 1-.817.816h-3.669a.816.816 0 0 1-.817-.817v-3.669zm-3.467-3.674c0-.337.276-.61.61-.61h2.45c.338 0 .611.276.611.61v2.45c0 .338-.276.611-.61.611h-2.45a.612.612 0 0 1-.611-.61v-2.45zM61.089 52.382a36.48 36.48 0 0 1-.463.073c-3.95.513-4.733-2.421-5.419-4.66L51.767 35c-.175-.654.229-1.183.907-1.183h.815c.676 0 1.366.528 1.542 1.182l3.443 12.809c.373.945.85 1.82 1.883 1.82 1.035 0 1.577-.214 2.05-1.977l2.986-12.643c.156-.658.83-1.191 1.507-1.191h.802c.677 0 1.095.534.935 1.19l-3.948 16.17c-1.817 7.135-4.153 8.173-7.927 8.323-1.152-.15-1.745-2.488-.805-2.866 0 0 1.902.013 3.002-.784 1.188-.862 1.693-2.24 1.968-3.026.016-.003.143-.397.162-.442zM47.37 41.363h-6.218a5.406 5.406 0 1 0 0 10.81h4.075a5.406 5.406 0 0 0 5.407-5.405v-7.852c0-5.507-5.92-5.507-7.547-5.507-2.104 0-4.697.318-4.697.318a.93.93 0 0 0-.81.904v1.227c0 .45.363.776.81.729 0 0 2.844-.322 4.539-.322 1.62 0 4.441 0 4.441 2.65v2.448zm-8.362 5.303a2.85 2.85 0 0 1 2.854-2.855h5.508v2.855a2.85 2.85 0 0 1-2.854 2.856h-2.654a2.857 2.857 0 0 1-2.854-2.856zM18 31.37a3.467 3.467 0 0 1 3.469-3.468h7.545a6.73 6.73 0 0 1 6.73 6.729v2.247a6.73 6.73 0 0 1-6.73 6.73h-7.547v7.545a1.02 1.02 0 0 1-1.021 1.02h-1.424A1.021 1.021 0 0 1 18 51.153V31.37zm3.467.795c0-.552.446-1 1.002-1h5.421a4.59 4.59 0 1 1 0 9.179h-6.423v-8.179z", + At::Fill => "#FFF" + ]] + ] + ] + } + + fn pay_on_spot(model: &crate::Model) -> Node { + div![ + C!["w-full p-3 border-b border-gray-200"], + label![ + C!["flex items-center cursor-pointer"], + attrs![At::For => "pay_on_spot"], + input![ + C!["form-radio h-5 w-5 text-indigo-500"], + attrs![At::Id => "pay_on_spot", At::Name => SELECTED_PAYMENT_METHOD, At::Type => "radio", At::Value => "pay_on_spot"], + ], + span![ + C!["flex items-center"], + pay_in_spot_icon(), + span![C!["ml-3"], model.i18n.t("Pay on spot")] + ] + ], + ] + } + + fn pay_in_spot_icon() -> Node { + svg![ + attrs![ + At::Style => "height: 21px; width: 33px;", + At::ViewBox => "0 0 31 17", + At::Xmlns => "http://www.w3.org/2000/svg" + ], + C!["ml-3"], + g![ + attrs!["transform" => "translate(120.5 -1344.862)"], + rect![attrs![ + At::Width => "31" , + At::Height => "17", + At::X => "-120.5", + At::Y => "1344.862", + At::Fill => "#2dbca4", + At::Rx => "1.5", + At::Ry => "1.5" + ]], + path![attrs![ + At::Fill => "#15a78f", + At::D => "M-115.5 1346.862a3 3 0 0 1-3 3v7a3 3 0 0 1 3 3h21a3 3 0 0 1 3-3v-7a3 3 0 0 1-3-3z" + ]], + circle![attrs![ + At::Cx => "-115", + At::Cy => "1353.362", + At::R => ".5", + At::Fill => "#2dbca4", + At::Stroke => "#0c5286", + At::StrokeLinecap => "round", + "stroke-miterlimit" => "3.7" + ]], + circle![attrs![ + At::Cx => "-95", + At::Cy => "1353.362", + At::R => ".5", + At::Fill => "#2dbca4", + At::Stroke => "#0c5286", + At::StrokeLinecap => "round", + "stroke-miterlimit" => "3.7" + ]], + path![attrs![ + At::Fill => "#2dbca4", + At::D => "M-99.5 1353.362c0 3.036-2.474 5.5-5.516 5.5a5.484 5.484 0 0 1-5.484-5.5c0-3.035 2.44-5.5 5.484-5.5 3.042 0 5.516 2.465 5.516 5.5z" + ]], + path![attrs![ + At::Fill => "#0c5286", + At::Style => "line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;isolation:auto;mix-blend-mode:normal", + At::D => "M-105.49 327.998v1.191a2.476 2.476 0 0 0-1.065.442c-.701.495-1.072 1.398-.812 2.256.297 1.033 1.312 1.631 2.31 1.613h.121a.5.5 0 0 0 .012 0c.571-.011 1.188.372 1.336.893a.5.5 0 0 0 .002.011c.122.398-.075.908-.432 1.157a.5.5 0 0 0-.015.01c-.6.448-1.647.327-2.082-.28a1.051 1.051 0 0 1-.201-.607h-1c-.001.433.142.85.39 1.195a2.36 2.36 0 0 0 1.436.92V338h1v-1.183a2.476 2.476 0 0 0 1.047-.436c.703-.494 1.074-1.397.814-2.256-.297-1.034-1.316-1.631-2.314-1.613h-.12a.5.5 0 0 0-.011 0c-.571.011-1.186-.372-1.334-.893a.5.5 0 0 0-.002-.011c-.122-.398.075-.908.432-1.157a.5.5 0 0 0 .013-.011c.6-.449 1.646-.326 2.08.281a.5.5 0 0 0 .002 0c.129.177.202.397.201.607h1a2.03 2.03 0 0 0-.388-1.195 2.36 2.36 0 0 0-1.42-.916v-1.219z", + At::Color => "#000", + At::FontFamily => "sans-serif", + At::FontWeight => "400", + At::Overflow => "visible", + At::Transform => "translate(0 1020.362)", + ]], + ] + ] + } +} diff --git a/web/src/pages/public/shopping_cart.rs b/web/src/pages/public/shopping_cart.rs index 87828af..0e5949f 100644 --- a/web/src/pages/public/shopping_cart.rs +++ b/web/src/pages/public/shopping_cart.rs @@ -70,69 +70,76 @@ pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node { fn summary(model: &crate::Model, page: &ShoppingCartPage) -> Node { div![ C!["my-4 mt-6 -mx-2 lg:flex"], - summary_left(model, page), + summary_left::view(model, page), summary_right::view(model, page) ] } -fn summary_left(model: &crate::Model, page: &ShoppingCartPage) -> Node { - div![ - C!["lg:px-2 lg:w-1/2"], - IF![model.config.coupons => div![C!["p-4 bg-gray-100 rounded-full"], h1![C!["ml-2 font-bold uppercase"], model.i18n.t("Coupon Code")]]], - IF![model.config.coupons => coupon_form(model, page)], - div![ - C!["p-4 bg-gray-100 rounded-full", IF![model.config.coupons => "mt-6"]], - div![ - C!["ml-2 font-bold uppercase"], - model.i18n.t("Instruction for seller") - ] - ], - div![C!["p-4"], p![C!["mb-4 italic"], model.i18n.t("If you have some information for the seller you can leave them in the box below")]], - textarea![C!["w-full h-24 p-2 bg-gray-100 rounded border-none"]] - ] -} +mod summary_left { + use seed::prelude::*; + use seed::*; -fn coupon_form(model: &crate::Model, _page: &ShoppingCartPage) -> Node { - div![ - C!["p-4"], - p![ - C!["mb-4 italic"], - model - .i18n - .t("If you have a coupon code, please enter it in the box below") - ], + use crate::pages::public::shopping_cart::ShoppingCartPage; + + pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node { div![ - C!["justify-center md:flex"], - form![ + C!["lg:px-2 lg:w-1/2"], + IF![model.config.coupons => div![C!["p-4 bg-gray-100 rounded-full"], h1![C!["ml-2 font-bold uppercase"], model.i18n.t("Coupon Code")]]], + IF![model.config.coupons => coupon_form(model, page)], + div![ + C!["p-4 bg-gray-100 rounded-full", IF![model.config.coupons => "mt-6"]], div![ - C!["flex items-center w-full h-13 pl-3 bg-white bg-gray-100 border rounded-full"], - input![C!["w-full bg-gray-100 outline-none appearance-none focus:outline-none active:outline-none"]], - button![ - C!["text-sm flex items-center px-3 py-1 text-white bg-gray-800 rounded-full outline-none md:px-4 hover:bg-gray-700 focus:outline-none active:outline-none"], - gift_icon(), - span![C!["font-medium"], model.i18n.t("Apply coupon")] + C!["ml-2 font-bold uppercase"], + model.i18n.t("Instruction for seller") + ] + ], + div![C!["p-4"], p![C!["mb-4 italic"], model.i18n.t("If you have some information for the seller you can leave them in the box below")]], + textarea![C!["w-full h-24 p-2 bg-gray-100 rounded border-none"]] + ] + } + + fn coupon_form(model: &crate::Model, _page: &ShoppingCartPage) -> Node { + div![ + C!["p-4"], + p![ + C!["mb-4 italic"], + model + .i18n + .t("If you have a coupon code, please enter it in the box below") + ], + div![ + C!["justify-center md:flex"], + form![ + div![ + C!["flex items-center w-full h-13 pl-3 bg-white bg-gray-100 border rounded-full"], + input![C!["w-full bg-gray-100 outline-none appearance-none focus:outline-none active:outline-none"]], + button![ + C!["text-sm flex items-center px-3 py-1 text-white bg-gray-800 rounded-full outline-none md:px-4 hover:bg-gray-700 focus:outline-none active:outline-none"], + gift_icon(), + span![C!["font-medium"], model.i18n.t("Apply coupon")] + ] ] ] ] ] - ] -} + } -fn gift_icon() -> Node { - svg![ - attrs![ - "aria-hidden" => "true", - "data-prefix" => "fas", - "data-icon" => "gift", - "class" => "w-8", - "xmlns" => "http://www.w3.org/2000/svg", - "viewBox" => "0 0 512 512" - ], - path![attrs![ - "fill" => "currentColor", - "d" => "M32 448c0 17.7 14.3 32 32 32h160V320H32v128zm256 32h160c17.7 0 32-14.3 32-32V320H288v160zm192-320h-42.1c6.2-12.1 10.1-25.5 10.1-40 0-48.5-39.5-88-88-88-41.6 0-68.5 21.3-103 68.3-34.5-47-61.4-68.3-103-68.3-48.5 0-88 39.5-88 88 0 14.5 3.8 27.9 10.1 40H32c-17.7 0-32 14.3-32 32v80c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16v-80c0-17.7-14.3-32-32-32zm-326.1 0c-22.1 0-40-17.9-40-40s17.9-40 40-40c19.9 0 34.6 3.3 86.1 80h-86.1zm206.1 0h-86.1c51.4-76.5 65.7-80 86.1-80 22.1 0 40 17.9 40 40s-17.9 40-40 40z" - ]] - ] + fn gift_icon() -> Node { + svg![ + attrs![ + "aria-hidden" => "true", + "data-prefix" => "fas", + "data-icon" => "gift", + "class" => "w-8", + At::Xmlns => "http://www.w3.org/2000/svg", + "viewBox" => "0 0 512 512" + ], + path![attrs![ + "fill" => "currentColor", + "d" => "M32 448c0 17.7 14.3 32 32 32h160V320H32v128zm256 32h160c17.7 0 32-14.3 32-32V320H288v160zm192-320h-42.1c6.2-12.1 10.1-25.5 10.1-40 0-48.5-39.5-88-88-88-41.6 0-68.5 21.3-103 68.3-34.5-47-61.4-68.3-103-68.3-48.5 0-88 39.5-88 88 0 14.5 3.8 27.9 10.1 40H32c-17.7 0-32 14.3-32 32v80c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16v-80c0-17.7-14.3-32-32-32zm-326.1 0c-22.1 0-40-17.9-40-40s17.9-40 40-40c19.9 0 34.6 3.3 86.1 80h-86.1zm206.1 0h-86.1c51.4-76.5 65.7-80 86.1-80 22.1 0 40 17.9 40 40s-17.9 40-40 40z" + ]] + ] + } } mod summary_right { @@ -307,13 +314,13 @@ mod summary_right { "aria-hidden" => "true", "data-prefix" => "far", "data-icon" => "trash-alt", - "class" => "w-4 text-red-600 hover:text-red-800", - "xmlns" => "http://www.w3.org/2000/svg", - "viewBox" => "0 0 448 512" + At::Xmlns => "http://www.w3.org/2000/svg", + At::ViewBox => "0 0 448 512" ], + C!["w-4 text-red-600 hover:text-red-800"], path![attrs![ - "fill" => "currentColor", - "d" => "M268 416h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12zM432 80h-82.41l-34-56.7A48 48 0 00274.41 0H173.59a48 48 0 00-41.16 23.3L98.41 80H16A16 16 0 000 96v16a16 16 0 0016 16h16v336a48 48 0 0048 48h288a48 48 0 0048-48V128h16a16 16 0 0016-16V96a16 16 0 00-16-16zM171.84 50.91A6 6 0 01177 48h94a6 6 0 015.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12z" + At::Fill => "currentColor", + At::D => "M268 416h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12zM432 80h-82.41l-34-56.7A48 48 0 00274.41 0H173.59a48 48 0 00-41.16 23.3L98.41 80H16A16 16 0 000 96v16a16 16 0 0016 16h16v336a48 48 0 0048 48h288a48 48 0 0048-48V128h16a16 16 0 0016-16V96a16 16 0 00-16-16zM171.84 50.91A6 6 0 01177 48h94a6 6 0 015.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12z" ]] ] } @@ -324,13 +331,13 @@ mod summary_right { "aria-hidden" => "true", "data-prefix" => "far", "data-icon" => "credit-card", - "class" => "w-8", - "xmlns" => "http://www.w3.org/2000/svg", - "viewBox" => "0 0 576 512" + At::Xmlns => "http://www.w3.org/2000/svg", + At::ViewBox => "0 0 576 512" ], + C!["w-8"], path![attrs![ - "fill" => "currentColor", - "d" => "M527.9 32H48.1C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48.1 48h479.8c26.6 0 48.1-21.5 48.1-48V80c0-26.5-21.5-48-48.1-48zM54.1 80h467.8c3.3 0 6 2.7 6 6v42H48.1V86c0-3.3 2.7-6 6-6zm467.8 352H54.1c-3.3 0-6-2.7-6-6V256h479.8v170c0 3.3-2.7 6-6 6zM192 332v40c0 6.6-5.4 12-12 12h-72c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h72c6.6 0 12 5.4 12 12zm192 0v40c0 6.6-5.4 12-12 12H236c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h136c6.6 0 12 5.4 12 12z" + At::Fill => "currentColor", + At::D => "M527.9 32H48.1C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48.1 48h479.8c26.6 0 48.1-21.5 48.1-48V80c0-26.5-21.5-48-48.1-48zM54.1 80h467.8c3.3 0 6 2.7 6 6v42H48.1V86c0-3.3 2.7-6 6-6zm467.8 352H54.1c-3.3 0-6-2.7-6-6V256h479.8v170c0 3.3-2.7 6-6 6zM192 332v40c0 6.6-5.4 12-12 12h-72c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h72c6.6 0 12 5.4 12 12zm192 0v40c0 6.6-5.4 12-12 12H236c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h136c6.6 0 12 5.4 12 12z" ]] ] } diff --git a/web/src/session.rs b/web/src/session.rs index b975863..5e12401 100644 --- a/web/src/session.rs +++ b/web/src/session.rs @@ -61,7 +61,7 @@ pub fn redirect_on_session(model: &Model, orders: &mut impl Orders) { PublicPage::SignIn(_) => {} PublicPage::SignUp(_) => {} PublicPage::ShoppingCart(_) => {} - PublicPage::Checkout => {} + PublicPage::Checkout(_) => {} }, } } diff --git a/web/src/shared/notification.rs b/web/src/shared/notification.rs index 811451f..699b953 100644 --- a/web/src/shared/notification.rs +++ b/web/src/shared/notification.rs @@ -162,7 +162,7 @@ pub fn message(id: uuid::Uuid, message: &str, icon: Type) -> Node { fn icon(color: &str, d: &str) -> Node { svg![ attrs![ - "xmlns"=>"http://www.w3.org/2000/svg", + At::Xmlns=>"http://www.w3.org/2000/svg", "class" => "w-8 h-8", "viewBox" => "0 0 20 20", "fill" => "currentColor" @@ -218,7 +218,7 @@ fn close_icon() -> Node { C!["inline-flex items-center cursor-pointer"], svg![ attrs![ - "xmlns" => "http://www.w3.org/2000/svg", + At::Xmlns => "http://www.w3.org/2000/svg", "class" => "w-4 h-4 text-gray-600", "fill" => "none", "viewBox" => "0 0 24 24", diff --git a/web/src/shared/view.rs b/web/src/shared/view.rs index 8863641..d6c7fe2 100644 --- a/web/src/shared/view.rs +++ b/web/src/shared/view.rs @@ -76,7 +76,7 @@ pub mod public_navbar { "width" => "32px", "height" => "32px", At::ViewBox => "0 0 32 32", - "xmlns" => "http://www.w3.org/2000/svg", + At::Xmlns => "http://www.w3.org/2000/svg", At::Class => "w-6 h-6", At::Fill => "none", At::Stroke => "currentColor", @@ -91,7 +91,7 @@ pub mod public_navbar { fn account() -> Node { svg![ attrs![ - "xmlns" => "http://www.w3.org/2000/svg", + At::Xmlns => "http://www.w3.org/2000/svg", At::Class => "w-6 h-6", At::Fill => "none", At::ViewBox => "0 0 24 24", @@ -180,7 +180,7 @@ pub mod cart_dropdown { fn trash_icon() -> Node { svg![ attrs![ - "xmlns" => Namespace::Svg.as_str(), + At::Xmlns => Namespace::Svg.as_str(), "width" => "100%", "height" => "100%", "fill" => "none",