From d5e675b6dc908ac3eed2db3448c78ed9d7858638 Mon Sep 17 00:00:00 2001 From: eraden Date: Mon, 16 May 2022 20:29:48 +0200 Subject: [PATCH] Admin landing, translations, shopping cart events --- shared/model/src/lib.rs | 6 ++ web/src/i18n/pl.rs | 9 +- web/src/lib.rs | 48 ++++++++++- web/src/pages.rs | 12 ++- web/src/pages/admin.rs | 14 +++- web/src/pages/admin/landing.rs | 116 +++++++++++++++++++++++++- web/src/pages/public/product.rs | 4 +- web/src/pages/public/shopping_cart.rs | 65 +++++++++++---- web/src/session.rs | 6 +- web/src/shopping_cart.rs | 29 ++++++- 10 files changed, 275 insertions(+), 34 deletions(-) diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index b2340ae..1c017a2 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -292,6 +292,12 @@ impl ops::Mul for Price { #[serde(transparent)] pub struct Quantity(NonNegative); +impl Quantity { + pub fn from_u32(v: u32) -> Self { + Self(NonNegative(v.try_into().unwrap_or_default())) + } +} + impl ops::Add for Quantity { type Output = Self; diff --git a/web/src/i18n/pl.rs b/web/src/i18n/pl.rs index b5a899c..17357e9 100644 --- a/web/src/i18n/pl.rs +++ b/web/src/i18n/pl.rs @@ -40,5 +40,12 @@ pub fn define(i18n: &mut I18n) { .define("Home", "Home") .define("Account", "Profil") .define("Shopping cart", "Koszyk") - .define("Qty:", "Ilość:"); + .define("Qty:", "Ilość:") + // shopping cart + .define("(Remove item)", "(Usuń)") + .define("Product", "Produkt") + .define("Quantity", "Ilość") + .define("Unit price", "Cena jednostkowa") + .define("Total price", "Cena łączna") + .define("Delivery", "Sposób dostawy"); } diff --git a/web/src/lib.rs b/web/src/lib.rs index e846c61..ad545c5 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -15,7 +15,7 @@ use seed::prelude::*; use crate::i18n::I18n; use crate::model::Model; -use crate::pages::{Msg, Page, PublicPage}; +use crate::pages::{AdminPage, Msg, Page, PublicPage}; use crate::session::SessionMsg; #[macro_export] @@ -30,6 +30,16 @@ macro_rules! fetch_page { _ => return $ret, } }}; + (admin $model: expr, $page: ident, $ret: expr) => {{ + let p = match &mut $model.page { + crate::pages::Page::Admin(p) => p, + _ => return $ret, + }; + match p { + crate::pages::AdminPage::$page(p) => p, + _ => return $ret, + } + }}; (public $model: expr, $page: ident) => {{ let p = match &mut $model.page { crate::pages::Page::Public(p) => p, @@ -40,6 +50,16 @@ macro_rules! fetch_page { _ => return, } }}; + (admin $model: expr, $page: ident) => {{ + let p = match &mut $model.page { + crate::pages::Page::Admin(p) => p, + _ => return, + }; + match p { + crate::pages::AdminPage::$page(p) => p, + _ => return, + } + }}; (public page $page: expr, $page_name: ident) => {{ let p = match $page { crate::pages::Page::Public(p) => p, @@ -66,6 +86,22 @@ macro_rules! fetch_page { } } }}; + (admin page $page: expr, $page_name: ident, $ret: expr) => {{ + let p = match $page { + crate::pages::Page::Admin(p) => p, + _ => { + *$page = $ret; + return; + } + }; + match p { + crate::pages::AdminPage::$page_name(p) => p, + _ => { + *$page = $ret; + return; + } + } + }}; } fn init(url: Url, orders: &mut impl Orders) -> Model { @@ -99,6 +135,10 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { } fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { + #[cfg(debug_assertions)] + if !matches!(msg, Msg::Session(SessionMsg::CheckSession)) { + seed::log!("msg", msg); + } match msg { #[cfg(debug_assertions)] Msg::NoOp => { @@ -135,7 +175,10 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { let page = fetch_page!(public model, ShoppingCart); pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into)) } - Msg::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)) + } Msg::Session(msg) => { session::update(msg, model, orders); } @@ -158,6 +201,7 @@ fn view(model: &Model) -> Node { Page::Public(PublicPage::ShoppingCart(page)) => { pages::public::shopping_cart::view(model, page) } + Page::Admin(AdminPage::Landing(page)) => pages::admin::landing::view(model, page), _ => empty![], }; diff --git a/web/src/pages.rs b/web/src/pages.rs index aa18272..d7efda0 100644 --- a/web/src/pages.rs +++ b/web/src/pages.rs @@ -22,7 +22,7 @@ pub enum Msg { #[derive(Debug)] pub enum AdminPage { - Landing, + Landing(admin::landing::SignInPage), Dashboard, Products, Product, @@ -70,7 +70,10 @@ impl Page { url, &mut orders.proxy(Into::into), ))), - ["admin"] => Self::Admin(AdminPage::Landing), + ["admin"] => Self::Admin(AdminPage::Landing(admin::landing::init( + url, + &mut orders.proxy(Into::into), + ))), _ => Self::Public(PublicPage::Listing(public::listing::init( url, &mut orders.proxy(Into::into), @@ -105,7 +108,10 @@ impl Page { let page = crate::fetch_page!(public page self, SignUp, Page::init(url, orders)); public::sign_up::page_changed(url, page); } - ["admin"] => {} + ["admin"] => { + let page = crate::fetch_page!(admin page self, Landing, Page::init(url, orders)); + admin::landing::page_changed(url, page); + } _ => {} } } diff --git a/web/src/pages/admin.rs b/web/src/pages/admin.rs index e68ee21..c6491ab 100644 --- a/web/src/pages/admin.rs +++ b/web/src/pages/admin.rs @@ -1,12 +1,12 @@ -mod landing; +pub mod landing; #[derive(Debug)] pub enum Msg { - Landing(landing::Msg), + Landing(landing::LogInMsg), } -impl From for Msg { - fn from(msg: landing::Msg) -> Self { +impl From for Msg { + fn from(msg: landing::LogInMsg) -> Self { Self::Landing(msg) } } @@ -16,3 +16,9 @@ impl From for crate::Msg { crate::Msg::Admin(msg) } } + +impl From for crate::Msg { + fn from(msg: landing::LogInMsg) -> Self { + Self::Admin(msg.into()) + } +} diff --git a/web/src/pages/admin/landing.rs b/web/src/pages/admin/landing.rs index 2510e99..612dd60 100644 --- a/web/src/pages/admin/landing.rs +++ b/web/src/pages/admin/landing.rs @@ -1,2 +1,114 @@ -#[derive(Debug, thiserror::Error)] -pub enum Msg {} +use seed::prelude::*; +use seed::*; + +use crate::pages::Urls; + +#[derive(Debug)] +pub enum LogInMsg { + LoginChanged(String), + PasswordChanged(String), + Submit, +} + +#[derive(Debug)] +pub struct SignInPage { + pub login: model::Login, + pub password: model::Password, +} + +pub fn init(mut _url: Url, _orders: &mut impl Orders) -> SignInPage { + SignInPage { + login: model::Login::new(""), + password: model::Password::new(""), + } +} + +pub fn page_changed(_url: Url, _model: &mut SignInPage) {} + +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(); + let admin_logo = model + .logo + .as_deref() + .map(|src| { + a![ + attrs![At::Href => home], + img![attrs![At::Src => src], C!["m-auto"]] + ] + }) + .unwrap_or_else(|| a![attrs![At::Href => home], "Home"]); + let content = div![ + C!["relative flex flex-col justify-center min-h-screen overflow-hidden"], + div![ + C!["w-full p-6 m-auto bg-white rounded shadow-lg ring-2 ring-indigo-800/50 lg:max-w-md"], + h2![ + C!["text-3xl font-semibold text-center text-indigo-700"], + admin_logo + ], + h1![C!["text-2xl font-semibold text-center text-indigo-700 m-3"], "Admin Panel"], + admin_sign_in_form(model, page), + p![ + C!["mt-8 text-xs font-light text-center text-indigo-700"], + model.i18n.t("Don't have an account?"), + a![ + C!["font-medium text-indigo-600 hover:underline"], + attrs![At::Href => Urls::new(&model.url).sign_up()], + " ", + model.i18n.t("Sign up") + ] + ] + ] + ] + .map_msg(Into::into); + + div![content] +} + +fn admin_sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node { + form![ + C!["mt-6"], + ev("submit", |ev| { + ev.stop_propagation(); + ev.prevent_default(); + LogInMsg::Submit + }), + div![ + label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")], + input![ + attrs!["type" => "text", "id" => "login"], + 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.prevent_default(); + ev.target().map(|target| seed::to_input(&target).value()).map(LogInMsg::LoginChanged) + }) + ] + ], + div![ + C!["mt-4"], + div![ + label![attrs!["for" => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")], + 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.prevent_default(); + ev.target().map(|target| seed::to_input(&target).value()).map(LogInMsg::PasswordChanged) + }) + ], + a![ + C!["text-xs text-indigo-600 hover:underline"], + attrs![At::Href => Urls::new(&model.url).forgot_password()], + model.i18n.t("Forget Password?"), + ], + div![ + C!["mt-6"], + button![ + C!["w-full px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-indigo-700 rounded-md hover:bg-indigo-600 focus:outline-none focus:bg-indigo-600"], + model.i18n.t("Log in") + ] + ] + ] + ] +} diff --git a/web/src/pages/public/product.rs b/web/src/pages/public/product.rs index 7088bf4..7277c52 100644 --- a/web/src/pages/public/product.rs +++ b/web/src/pages/public/product.rs @@ -41,10 +41,10 @@ pub fn update(msg: ProductMsg, model: &mut ProductPage, _orders: &mut impl Order model.products.update(products.0); } ProductMsg::ProductsFetched(NetRes::Error(e)) => { - seed::error!(e); + seed::error!("fetch product error", e); } ProductMsg::ProductsFetched(NetRes::Http(e)) => { - seed::error!(e); + seed::error!("fetch product http", e); } ProductMsg::SelectImage(selected) => { model.selected_image = selected; diff --git a/web/src/pages/public/shopping_cart.rs b/web/src/pages/public/shopping_cart.rs index eadf2b2..8d8d08f 100644 --- a/web/src/pages/public/shopping_cart.rs +++ b/web/src/pages/public/shopping_cart.rs @@ -1,8 +1,10 @@ -use rusty_money::Money; +use model::Quantity; use seed::prelude::*; use seed::*; use crate::api::NetRes; +use crate::pages::Urls; +use crate::shopping_cart::CartMsg; #[derive(Debug)] pub enum ShoppingCartMsg { @@ -37,21 +39,26 @@ pub fn update( model.products.update(products.0); } ShoppingCartMsg::ProductsFetched(NetRes::Error(e)) => { - seed::error!(e); + seed::error!("fetch product error", e); } ShoppingCartMsg::ProductsFetched(NetRes::Http(e)) => { - seed::error!(e); + seed::error!("fetch product http", e); } } } pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node { - div![ + 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"], products(model, page) ] + ]; + + div![ + crate::shared::view::public_navbar::view(model, &page.products), + super::layout::view(model, content, None) ] } @@ -122,11 +129,17 @@ fn item_view( .map(|photo| photo.url.as_str()) .unwrap_or_default(); + let product_id = product.id; + let quantity_unit = product.quantity_unit; + let product_url = Urls::new(&model.url) + .product() + .add_path_part(product.id.to_string()); + tr![ td![ C!["hidden pb-4 md:table-cell"], a![ - attrs![At::Href => "#"], + attrs![At::Href => product_url.clone()], img![attrs![ At::Src => img, At::Class => "w-20 rounded", @@ -135,13 +148,22 @@ fn item_view( ] ], td![a![ - attrs![At::Href => "#"], + attrs![At::Href => product_url.clone()], p![C!["mb-2 md:ml-4"], product.name.as_str()], form![ - attrs![ "action" => "", "method" => "POST"], + ev(Ev::Submit, move |ev| { + ev.prevent_default(); + ev.stop_propagation(); + crate::Msg::from(CartMsg::Remove(product_id)) + }), button![ - attrs!["type" => "submit", "class" => "text-gray-700 md:ml-4"], - small![model.i18n.t("(Remove item)]")] + attrs![At::Type => "submit", At::Class => "text-gray-700 md:ml-4 text-red-600"], + small![model.i18n.t("(Remove item)")], + ev(Ev::Click, move |ev| { + ev.prevent_default(); + ev.stop_propagation(); + crate::Msg::from(CartMsg::Remove(product_id)) + }) ] ] ]], @@ -151,11 +173,26 @@ fn item_view( 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" - ]], + 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" + ], + ev(Ev::Change, move |ev| { + ev.stop_propagation(); + let target = ev.target()?; + let input = seed::to_input(&target); + let value: u32 = input.value().parse().ok()?; + let quantity = Quantity::from_u32(value); + + Some(crate::Msg::from(CartMsg::ModifyItem { + product_id, + quantity_unit, + quantity, + })) + }) + ], ] ] ], diff --git a/web/src/session.rs b/web/src/session.rs index 84e5de3..baca0d1 100644 --- a/web/src/session.rs +++ b/web/src/session.rs @@ -38,10 +38,10 @@ pub fn init(model: &mut Model, orders: &mut impl Orders) { } pub fn redirect_on_session(model: &Model, orders: &mut impl Orders) { - seed::log!(&model.page, model.shared.me.is_some()); + // seed::log!(&model.page, model.shared.me.is_some()); match &model.page { Page::Admin(admin) => match admin { - AdminPage::Landing => {} + AdminPage::Landing(_) => {} AdminPage::Dashboard => {} AdminPage::Products => {} AdminPage::Product => {} @@ -152,7 +152,7 @@ pub fn update(msg: SessionMsg, model: &mut Model, orders: &mut impl Orders) seed::error!("net", net); } FetchError::RequestError(e) => { - seed::error!(e); + seed::error!("request error", e); } FetchError::StatusError(status) => match status.code { 401 | 403 => { diff --git a/web/src/shopping_cart.rs b/web/src/shopping_cart.rs index 33eec8e..bc859e5 100644 --- a/web/src/shopping_cart.rs +++ b/web/src/shopping_cart.rs @@ -16,6 +16,7 @@ pub enum CartMsg { quantity_unit: QuantityUnit, product_id: ProductId, }, + Remove(ProductId), Hover, Leave, } @@ -57,7 +58,7 @@ pub fn update(msg: CartMsg, model: &mut Model, _orders: &mut impl Orders) { { let items: &mut Items = &mut model.cart.items; let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item { - quantity, + quantity: Quantity::from_u32(0), quantity_unit, product_id, }); @@ -66,7 +67,29 @@ pub fn update(msg: CartMsg, model: &mut Model, _orders: &mut impl Orders) { } store_local(&model.cart); } - CartMsg::ModifyItem { .. } => {} + CartMsg::ModifyItem { + product_id, + quantity_unit, + quantity, + } => { + if **quantity == 0 { + model.cart.items.remove(&product_id); + } else { + let items: &mut Items = &mut model.cart.items; + let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item { + quantity, + quantity_unit, + product_id, + }); + entry.quantity = quantity; + entry.quantity_unit = quantity_unit; + } + store_local(&model.cart); + } + CartMsg::Remove(product_id) => { + model.cart.items.remove(&product_id); + store_local(&model.cart); + } CartMsg::Hover => { model.cart.hover = true; } @@ -80,7 +103,7 @@ fn load_local() -> ShoppingCart { match LocalStorage::get("ct") { Ok(cart) => cart, Err(e) => { - seed::error!(e); + seed::error!("Storage error", e); ShoppingCart::default() } }