Admin landing, translations, shopping cart events

This commit is contained in:
eraden 2022-05-16 20:29:48 +02:00
parent 4db6cf7327
commit d5e675b6dc
10 changed files with 275 additions and 34 deletions

View File

@ -292,6 +292,12 @@ impl ops::Mul<Quantity> for Price {
#[serde(transparent)] #[serde(transparent)]
pub struct Quantity(NonNegative); 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 { impl ops::Add for Quantity {
type Output = Self; type Output = Self;

View File

@ -40,5 +40,12 @@ pub fn define(i18n: &mut I18n) {
.define("Home", "Home") .define("Home", "Home")
.define("Account", "Profil") .define("Account", "Profil")
.define("Shopping cart", "Koszyk") .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");
} }

View File

@ -15,7 +15,7 @@ use seed::prelude::*;
use crate::i18n::I18n; use crate::i18n::I18n;
use crate::model::Model; use crate::model::Model;
use crate::pages::{Msg, Page, PublicPage}; use crate::pages::{AdminPage, Msg, Page, PublicPage};
use crate::session::SessionMsg; use crate::session::SessionMsg;
#[macro_export] #[macro_export]
@ -30,6 +30,16 @@ macro_rules! fetch_page {
_ => return $ret, _ => 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) => {{ (public $model: expr, $page: ident) => {{
let p = match &mut $model.page { let p = match &mut $model.page {
crate::pages::Page::Public(p) => p, crate::pages::Page::Public(p) => p,
@ -40,6 +50,16 @@ macro_rules! fetch_page {
_ => return, _ => 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) => {{ (public page $page: expr, $page_name: ident) => {{
let p = match $page { let p = match $page {
crate::pages::Page::Public(p) => p, 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<Msg>) -> Model { fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
@ -99,6 +135,10 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
} }
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
#[cfg(debug_assertions)]
if !matches!(msg, Msg::Session(SessionMsg::CheckSession)) {
seed::log!("msg", msg);
}
match msg { match msg {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
Msg::NoOp => { Msg::NoOp => {
@ -135,7 +175,10 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let page = fetch_page!(public model, ShoppingCart); let page = fetch_page!(public model, ShoppingCart);
pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into)) 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) => { Msg::Session(msg) => {
session::update(msg, model, orders); session::update(msg, model, orders);
} }
@ -158,6 +201,7 @@ fn view(model: &Model) -> Node<Msg> {
Page::Public(PublicPage::ShoppingCart(page)) => { Page::Public(PublicPage::ShoppingCart(page)) => {
pages::public::shopping_cart::view(model, page) pages::public::shopping_cart::view(model, page)
} }
Page::Admin(AdminPage::Landing(page)) => pages::admin::landing::view(model, page),
_ => empty![], _ => empty![],
}; };

View File

@ -22,7 +22,7 @@ pub enum Msg {
#[derive(Debug)] #[derive(Debug)]
pub enum AdminPage { pub enum AdminPage {
Landing, Landing(admin::landing::SignInPage),
Dashboard, Dashboard,
Products, Products,
Product, Product,
@ -70,7 +70,10 @@ impl Page {
url, url,
&mut orders.proxy(Into::into), &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( _ => Self::Public(PublicPage::Listing(public::listing::init(
url, url,
&mut orders.proxy(Into::into), &mut orders.proxy(Into::into),
@ -105,7 +108,10 @@ impl Page {
let page = crate::fetch_page!(public page self, SignUp, Page::init(url, orders)); let page = crate::fetch_page!(public page self, SignUp, Page::init(url, orders));
public::sign_up::page_changed(url, page); 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);
}
_ => {} _ => {}
} }
} }

View File

@ -1,12 +1,12 @@
mod landing; pub mod landing;
#[derive(Debug)] #[derive(Debug)]
pub enum Msg { pub enum Msg {
Landing(landing::Msg), Landing(landing::LogInMsg),
} }
impl From<landing::Msg> for Msg { impl From<landing::LogInMsg> for Msg {
fn from(msg: landing::Msg) -> Self { fn from(msg: landing::LogInMsg) -> Self {
Self::Landing(msg) Self::Landing(msg)
} }
} }
@ -16,3 +16,9 @@ impl From<Msg> for crate::Msg {
crate::Msg::Admin(msg) crate::Msg::Admin(msg)
} }
} }
impl From<landing::LogInMsg> for crate::Msg {
fn from(msg: landing::LogInMsg) -> Self {
Self::Admin(msg.into())
}
}

View File

@ -1,2 +1,114 @@
#[derive(Debug, thiserror::Error)] use seed::prelude::*;
pub enum Msg {} 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<LogInMsg>) -> 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<crate::Msg>) {}
pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
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<LogInMsg> {
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")
]
]
]
]
}

View File

@ -41,10 +41,10 @@ pub fn update(msg: ProductMsg, model: &mut ProductPage, _orders: &mut impl Order
model.products.update(products.0); model.products.update(products.0);
} }
ProductMsg::ProductsFetched(NetRes::Error(e)) => { ProductMsg::ProductsFetched(NetRes::Error(e)) => {
seed::error!(e); seed::error!("fetch product error", e);
} }
ProductMsg::ProductsFetched(NetRes::Http(e)) => { ProductMsg::ProductsFetched(NetRes::Http(e)) => {
seed::error!(e); seed::error!("fetch product http", e);
} }
ProductMsg::SelectImage(selected) => { ProductMsg::SelectImage(selected) => {
model.selected_image = selected; model.selected_image = selected;

View File

@ -1,8 +1,10 @@
use rusty_money::Money; use model::Quantity;
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
use crate::api::NetRes; use crate::api::NetRes;
use crate::pages::Urls;
use crate::shopping_cart::CartMsg;
#[derive(Debug)] #[derive(Debug)]
pub enum ShoppingCartMsg { pub enum ShoppingCartMsg {
@ -37,21 +39,26 @@ pub fn update(
model.products.update(products.0); model.products.update(products.0);
} }
ShoppingCartMsg::ProductsFetched(NetRes::Error(e)) => { ShoppingCartMsg::ProductsFetched(NetRes::Error(e)) => {
seed::error!(e); seed::error!("fetch product error", e);
} }
ShoppingCartMsg::ProductsFetched(NetRes::Http(e)) => { ShoppingCartMsg::ProductsFetched(NetRes::Http(e)) => {
seed::error!(e); seed::error!("fetch product http", e);
} }
} }
} }
pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> { pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
div![ let content = div![
C!["flex justify-center my-6"], C!["flex justify-center my-6"],
div![ 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"], 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) 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()) .map(|photo| photo.url.as_str())
.unwrap_or_default(); .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![ tr![
td![ td![
C!["hidden pb-4 md:table-cell"], C!["hidden pb-4 md:table-cell"],
a![ a![
attrs![At::Href => "#"], attrs![At::Href => product_url.clone()],
img![attrs![ img![attrs![
At::Src => img, At::Src => img,
At::Class => "w-20 rounded", At::Class => "w-20 rounded",
@ -135,13 +148,22 @@ fn item_view(
] ]
], ],
td![a![ td![a![
attrs![At::Href => "#"], attrs![At::Href => product_url.clone()],
p![C!["mb-2 md:ml-4"], product.name.as_str()], p![C!["mb-2 md:ml-4"], product.name.as_str()],
form![ form![
attrs![ "action" => "", "method" => "POST"], ev(Ev::Submit, move |ev| {
ev.prevent_default();
ev.stop_propagation();
crate::Msg::from(CartMsg::Remove(product_id))
}),
button![ button![
attrs!["type" => "submit", "class" => "text-gray-700 md:ml-4"], attrs![At::Type => "submit", At::Class => "text-gray-700 md:ml-4 text-red-600"],
small![model.i18n.t("(Remove item)]")] 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"], C!["w-20 h-10"],
div![ div![
C!["relative flex flex-row w-full h-8"], C!["relative flex flex-row w-full h-8"],
input![attrs![ input![
attrs![
At::Type => "number", At::Type => "number",
At::Value => **item.quantity, 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" 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,
}))
})
],
] ]
] ]
], ],

View File

@ -38,10 +38,10 @@ pub fn init(model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
pub fn redirect_on_session(model: &Model, orders: &mut impl Orders<Msg>) { pub fn redirect_on_session(model: &Model, orders: &mut impl Orders<Msg>) {
seed::log!(&model.page, model.shared.me.is_some()); // seed::log!(&model.page, model.shared.me.is_some());
match &model.page { match &model.page {
Page::Admin(admin) => match admin { Page::Admin(admin) => match admin {
AdminPage::Landing => {} AdminPage::Landing(_) => {}
AdminPage::Dashboard => {} AdminPage::Dashboard => {}
AdminPage::Products => {} AdminPage::Products => {}
AdminPage::Product => {} AdminPage::Product => {}
@ -152,7 +152,7 @@ pub fn update(msg: SessionMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
seed::error!("net", net); seed::error!("net", net);
} }
FetchError::RequestError(e) => { FetchError::RequestError(e) => {
seed::error!(e); seed::error!("request error", e);
} }
FetchError::StatusError(status) => match status.code { FetchError::StatusError(status) => match status.code {
401 | 403 => { 401 | 403 => {

View File

@ -16,6 +16,7 @@ pub enum CartMsg {
quantity_unit: QuantityUnit, quantity_unit: QuantityUnit,
product_id: ProductId, product_id: ProductId,
}, },
Remove(ProductId),
Hover, Hover,
Leave, Leave,
} }
@ -57,7 +58,7 @@ pub fn update(msg: CartMsg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
{ {
let items: &mut Items = &mut model.cart.items; let items: &mut Items = &mut model.cart.items;
let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item { let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item {
quantity, quantity: Quantity::from_u32(0),
quantity_unit, quantity_unit,
product_id, product_id,
}); });
@ -66,7 +67,29 @@ pub fn update(msg: CartMsg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
} }
store_local(&model.cart); 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 => { CartMsg::Hover => {
model.cart.hover = true; model.cart.hover = true;
} }
@ -80,7 +103,7 @@ fn load_local() -> ShoppingCart {
match LocalStorage::get("ct") { match LocalStorage::get("ct") {
Ok(cart) => cart, Ok(cart) => cart,
Err(e) => { Err(e) => {
seed::error!(e); seed::error!("Storage error", e);
ShoppingCart::default() ShoppingCart::default()
} }
} }