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)]
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;

View File

@ -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");
}

View File

@ -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<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>) {
#[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<Msg>) {
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<Msg> {
Page::Public(PublicPage::ShoppingCart(page)) => {
pages::public::shopping_cart::view(model, page)
}
Page::Admin(AdminPage::Landing(page)) => pages::admin::landing::view(model, page),
_ => empty![],
};

View File

@ -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);
}
_ => {}
}
}

View File

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

View File

@ -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<crate::Msg> {
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,
}))
})
],
]
]
],

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>) {
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<Msg>)
seed::error!("net", net);
}
FetchError::RequestError(e) => {
seed::error!(e);
seed::error!("request error", e);
}
FetchError::StatusError(status) => match status.code {
401 | 403 => {

View File

@ -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<Msg>) {
{
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<Msg>) {
}
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()
}
}