Shopping cart view, drop down shopping cart
This commit is contained in:
parent
ad924abd3f
commit
4db6cf7327
75
Cargo.lock
generated
75
Cargo.lock
generated
@ -1570,8 +1570,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"libgit2-sys",
|
"libgit2-sys",
|
||||||
"log",
|
"log",
|
||||||
"openssl-probe",
|
|
||||||
"openssl-sys",
|
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2055,26 +2053,10 @@ checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
"libssh2-sys",
|
|
||||||
"libz-sys",
|
"libz-sys",
|
||||||
"openssl-sys",
|
|
||||||
"pkg-config",
|
"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]]
|
[[package]]
|
||||||
name = "libz-sys"
|
name = "libz-sys"
|
||||||
version = "1.1.6"
|
version = "1.1.6"
|
||||||
@ -2225,15 +2207,6 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minidom"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e"
|
|
||||||
dependencies = [
|
|
||||||
"quick-xml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -2802,15 +2775,6 @@ version = "1.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@ -3200,42 +3164,6 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -4358,8 +4286,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"model",
|
"model",
|
||||||
"rusty-money",
|
"rusty-money",
|
||||||
"seed 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"seed",
|
||||||
"seed_heroicons",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -7,6 +7,7 @@ mod dummy;
|
|||||||
pub mod encrypt;
|
pub mod encrypt;
|
||||||
|
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::ops;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use derive_more::{Deref, DerefMut, Display, From};
|
use derive_more::{Deref, DerefMut, Display, From};
|
||||||
@ -276,6 +277,14 @@ impl Default for Audience {
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Price(NonNegative);
|
pub struct Price(NonNegative);
|
||||||
|
|
||||||
|
impl ops::Mul<Quantity> 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 = "dummy", derive(fake::Dummy))]
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||||
@ -283,6 +292,22 @@ pub struct Price(NonNegative);
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Quantity(NonNegative);
|
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<i32> for Quantity {
|
impl TryFrom<i32> for Quantity {
|
||||||
type Error = TransformError;
|
type Error = TransformError;
|
||||||
|
|
||||||
@ -366,6 +391,22 @@ impl<'de> serde::Deserialize<'de> for Email {
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct NonNegative(i32);
|
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<i32> for NonNegative {
|
impl TryFrom<i32> for NonNegative {
|
||||||
type Error = TransformError;
|
type Error = TransformError;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ crate-type = ["cdylib"]
|
|||||||
model = { path = "../shared/model", features = ["dummy"] }
|
model = { path = "../shared/model", features = ["dummy"] }
|
||||||
|
|
||||||
seed = { version = "0.9.1", features = [] }
|
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"] }
|
chrono = { version = "*", features = ["wasm-bindgen", "wasmbind"] }
|
||||||
gloo-timers = { version = "*", features = ["futures"] }
|
gloo-timers = { version = "*", features = ["futures"] }
|
||||||
|
@ -35,5 +35,10 @@ pub fn define(i18n: &mut I18n) {
|
|||||||
"There was internal server error. Please try later",
|
"There was internal server error. Please try later",
|
||||||
"Wystąpił błąd, proszę spróbować później",
|
"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ść:");
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
|||||||
shared: shared::Model::default(),
|
shared: shared::Model::default(),
|
||||||
i18n: I18n::load(),
|
i18n: I18n::load(),
|
||||||
cart: Default::default(),
|
cart: Default::default(),
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
debug_modal: false,
|
debug_modal: false,
|
||||||
};
|
};
|
||||||
@ -119,9 +120,9 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
let page = fetch_page!(public model, SignIn);
|
let page = fetch_page!(public model, SignIn);
|
||||||
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
|
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
|
||||||
}
|
}
|
||||||
Msg::Public(pages::public::Msg::SignUp(pages::public::sign_up::Msg::AccountCreated(
|
Msg::Public(pages::public::Msg::SignUp(
|
||||||
res,
|
pages::public::sign_up::RegisterMsg::AccountCreated(res),
|
||||||
))) => {
|
)) => {
|
||||||
orders
|
orders
|
||||||
.skip()
|
.skip()
|
||||||
.send_msg(Msg::Session(SessionMsg::TokenRefreshed(res)));
|
.send_msg(Msg::Session(SessionMsg::TokenRefreshed(res)));
|
||||||
@ -130,17 +131,21 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
let page = fetch_page!(public model, SignUp);
|
let page = fetch_page!(public model, SignUp);
|
||||||
pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into))
|
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::Admin(_) => {}
|
||||||
Msg::Session(msg) => {
|
Msg::Session(msg) => {
|
||||||
session::update(msg, model, orders);
|
session::update(msg, model, orders);
|
||||||
}
|
}
|
||||||
|
Msg::Cart(msg) => {
|
||||||
|
shopping_cart::update(msg, model, orders);
|
||||||
|
}
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
Msg::Debug(msg) => {
|
Msg::Debug(msg) => {
|
||||||
debug::update(msg, model);
|
debug::update(msg, model);
|
||||||
}
|
}
|
||||||
Msg::Cart(msg) => {
|
|
||||||
shopping_cart::update(msg, model, orders);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +155,9 @@ fn view(model: &Model) -> Node<Msg> {
|
|||||||
Page::Public(PublicPage::Product(page)) => pages::public::product::view(model, page),
|
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::SignIn(page)) => pages::public::sign_in::view(model, page),
|
||||||
Page::Public(PublicPage::SignUp(page)) => pages::public::sign_up::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![],
|
_ => empty![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use seed::Url;
|
use seed::Url;
|
||||||
|
|
||||||
use crate::{I18n, Page, shopping_cart};
|
use crate::{shopping_cart, I18n, Page};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
@ -11,6 +13,51 @@ pub struct Model {
|
|||||||
pub shared: crate::shared::Model,
|
pub shared: crate::shared::Model,
|
||||||
pub i18n: I18n,
|
pub i18n: I18n,
|
||||||
pub cart: shopping_cart::ShoppingCart,
|
pub cart: shopping_cart::ShoppingCart,
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub debug_modal: bool,
|
pub debug_modal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Products {
|
||||||
|
pub categories: Vec<model::api::Category>,
|
||||||
|
pub product_ids: Vec<model::ProductId>,
|
||||||
|
pub products: HashMap<model::ProductId, model::api::Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Products {
|
||||||
|
pub fn update(&mut self, products: Vec<model::api::Product>) {
|
||||||
|
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<F>(&self, filter: F) -> Vec<model::ProductId>
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -34,7 +34,7 @@ pub enum PublicPage {
|
|||||||
Product(public::product::ProductPage),
|
Product(public::product::ProductPage),
|
||||||
SignIn(public::sign_in::SignInPage),
|
SignIn(public::sign_in::SignInPage),
|
||||||
SignUp(public::sign_up::SignUpPage),
|
SignUp(public::sign_up::SignUpPage),
|
||||||
ShoppingCart,
|
ShoppingCart(public::shopping_cart::ShoppingCartPage),
|
||||||
Checkout,
|
Checkout,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +59,9 @@ impl Page {
|
|||||||
url,
|
url,
|
||||||
&mut orders.proxy(Into::into),
|
&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(
|
["sign-in", _rest @ ..] => Self::Public(PublicPage::SignIn(public::sign_in::init(
|
||||||
url,
|
url,
|
||||||
&mut orders.proxy(Into::into),
|
&mut orders.proxy(Into::into),
|
||||||
@ -89,6 +92,11 @@ impl Page {
|
|||||||
let page = crate::fetch_page!(public page self, Product, Page::init(url, orders));
|
let page = crate::fetch_page!(public page self, Product, Page::init(url, orders));
|
||||||
public::product::page_changed(url, page);
|
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", ..] => {
|
["sign-in", ..] => {
|
||||||
let page = crate::fetch_page!(public page self, SignIn, Page::init(url, orders));
|
let page = crate::fetch_page!(public page self, SignIn, Page::init(url, orders));
|
||||||
public::sign_in::page_changed(url, page);
|
public::sign_in::page_changed(url, page);
|
||||||
@ -131,6 +139,10 @@ impl<'a> Urls<'a> {
|
|||||||
self.base_url().add_path_part("sign-in")
|
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 {
|
pub fn sign_up(self) -> Url {
|
||||||
self.base_url().add_path_part("sign-up")
|
self.base_url().add_path_part("sign-up")
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,78 @@
|
|||||||
pub mod listing;
|
pub mod listing;
|
||||||
pub mod product;
|
pub mod product;
|
||||||
|
pub mod shopping_cart;
|
||||||
pub mod sign_in;
|
pub mod sign_in;
|
||||||
pub(crate) mod sign_up;
|
pub mod sign_up;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
Listing(listing::Msg),
|
Listing(listing::ListingMsg),
|
||||||
Product(product::Msg),
|
Product(product::ProductMsg),
|
||||||
SignIn(sign_in::Msg),
|
SignIn(sign_in::LogInMsg),
|
||||||
SignUp(sign_up::Msg),
|
SignUp(sign_up::RegisterMsg),
|
||||||
|
ShoppingCart(shopping_cart::ShoppingCartMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<listing::Msg> for Msg {
|
impl From<listing::ListingMsg> for Msg {
|
||||||
fn from(msg: listing::Msg) -> Self {
|
fn from(msg: listing::ListingMsg) -> Self {
|
||||||
Self::Listing(msg)
|
Self::Listing(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<product::Msg> for Msg {
|
impl From<product::ProductMsg> for Msg {
|
||||||
fn from(msg: product::Msg) -> Self {
|
fn from(msg: product::ProductMsg) -> Self {
|
||||||
Self::Product(msg)
|
Self::Product(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sign_in::Msg> for Msg {
|
impl From<sign_in::LogInMsg> for Msg {
|
||||||
fn from(msg: sign_in::Msg) -> Self {
|
fn from(msg: sign_in::LogInMsg) -> Self {
|
||||||
Self::SignIn(msg)
|
Self::SignIn(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sign_up::Msg> for Msg {
|
impl From<sign_up::RegisterMsg> for Msg {
|
||||||
fn from(msg: sign_up::Msg) -> Self {
|
fn from(msg: sign_up::RegisterMsg) -> Self {
|
||||||
Self::SignUp(msg)
|
Self::SignUp(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<listing::Msg> for crate::Msg {
|
impl From<shopping_cart::ShoppingCartMsg> for Msg {
|
||||||
fn from(msg: listing::Msg) -> Self {
|
fn from(msg: shopping_cart::ShoppingCartMsg) -> Self {
|
||||||
|
Self::ShoppingCart(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<listing::ListingMsg> for crate::Msg {
|
||||||
|
fn from(msg: listing::ListingMsg) -> Self {
|
||||||
crate::Msg::Public(msg.into())
|
crate::Msg::Public(msg.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<product::Msg> for crate::Msg {
|
impl From<product::ProductMsg> for crate::Msg {
|
||||||
fn from(msg: product::Msg) -> Self {
|
fn from(msg: product::ProductMsg) -> Self {
|
||||||
crate::Msg::Public(msg.into())
|
crate::Msg::Public(msg.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sign_in::Msg> for crate::Msg {
|
impl From<sign_in::LogInMsg> for crate::Msg {
|
||||||
fn from(msg: sign_in::Msg) -> Self {
|
fn from(msg: sign_in::LogInMsg) -> Self {
|
||||||
crate::Msg::Public(msg.into())
|
crate::Msg::Public(msg.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sign_up::Msg> for crate::Msg {
|
impl From<sign_up::RegisterMsg> for crate::Msg {
|
||||||
fn from(msg: sign_up::Msg) -> Self {
|
fn from(msg: sign_up::RegisterMsg) -> Self {
|
||||||
crate::Msg::Public(msg.into())
|
crate::Msg::Public(msg.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<shopping_cart::ShoppingCartMsg> for crate::Msg {
|
||||||
|
fn from(msg: shopping_cart::ShoppingCartMsg) -> Self {
|
||||||
|
Self::Public(msg.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Msg> for crate::Msg {
|
impl From<Msg> for crate::Msg {
|
||||||
fn from(msg: Msg) -> Self {
|
fn from(msg: Msg) -> Self {
|
||||||
crate::Msg::Public(msg)
|
crate::Msg::Public(msg)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use model::Quantity;
|
use model::Quantity;
|
||||||
use seed::app::Orders;
|
use seed::app::Orders;
|
||||||
@ -6,33 +6,31 @@ use seed::prelude::*;
|
|||||||
use seed::*;
|
use seed::*;
|
||||||
|
|
||||||
use crate::api::NetRes;
|
use crate::api::NetRes;
|
||||||
|
use crate::model::Products;
|
||||||
use crate::pages::Urls;
|
use crate::pages::Urls;
|
||||||
use crate::shopping_cart::CartMsg;
|
use crate::shopping_cart::CartMsg;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ListingPage {
|
pub struct ListingPage {
|
||||||
pub product_ids: Vec<model::ProductId>,
|
|
||||||
pub products: HashMap<model::ProductId, model::api::Product>,
|
|
||||||
pub errors: Vec<String>,
|
pub errors: Vec<String>,
|
||||||
pub categories: Vec<model::api::Category>,
|
|
||||||
pub filters: HashSet<String>,
|
pub filters: HashSet<String>,
|
||||||
pub visible_products: Vec<model::ProductId>,
|
pub visible_products: Vec<model::ProductId>,
|
||||||
|
|
||||||
|
pub products: Products,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum ListingMsg {
|
||||||
FetchProducts,
|
FetchProducts,
|
||||||
ProductFetched(NetRes<model::api::Products>),
|
ProductsFetched(NetRes<model::api::Products>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> ListingPage {
|
pub fn init(url: Url, orders: &mut impl Orders<ListingMsg>) -> ListingPage {
|
||||||
orders.send_msg(Msg::FetchProducts);
|
orders.send_msg(ListingMsg::FetchProducts);
|
||||||
ListingPage {
|
ListingPage {
|
||||||
product_ids: vec![],
|
products: Products::default(),
|
||||||
filters: url_to_filters(url),
|
filters: url_to_filters(url),
|
||||||
products: Default::default(),
|
|
||||||
errors: vec![],
|
errors: vec![],
|
||||||
categories: vec![],
|
|
||||||
visible_products: vec![],
|
visible_products: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,62 +51,41 @@ fn url_to_filters(mut url: Url) -> HashSet<String> {
|
|||||||
|
|
||||||
pub fn page_changed(url: Url, model: &mut ListingPage) {
|
pub fn page_changed(url: Url, model: &mut ListingPage) {
|
||||||
model.filters = url_to_filters(url);
|
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) {
|
fn filter_product(model: &ListingPage, product: &model::api::Product) -> bool {
|
||||||
model.visible_products = model
|
product
|
||||||
.product_ids
|
.category
|
||||||
.iter()
|
.as_ref()
|
||||||
.filter_map(|id| {
|
.filter(|c| model.filters.contains(c.key.as_str()))
|
||||||
model.products.get(id).and_then(|p| {
|
.is_some()
|
||||||
p.category
|
|
||||||
.as_ref()
|
|
||||||
.filter(|c| model.filters.contains(c.key.as_str()))
|
|
||||||
.map(|_| p.id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<crate::Msg>) {
|
pub fn update(msg: ListingMsg, model: &mut ListingPage, orders: &mut impl Orders<crate::Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::FetchProducts => {
|
ListingMsg::FetchProducts => {
|
||||||
orders.skip().perform_cmd({
|
orders.skip().perform_cmd({
|
||||||
async {
|
async {
|
||||||
crate::Msg::Public(
|
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)) => {
|
ListingMsg::ProductsFetched(NetRes::Success(products)) => {
|
||||||
model.categories = products
|
model.products.update(products.0);
|
||||||
.0
|
let ids = model
|
||||||
.iter()
|
.products
|
||||||
.fold(HashSet::new(), |mut set, p| {
|
.filter_product_ids(|product| filter_product(model, product));
|
||||||
if let Some(category) = p.category.as_ref().cloned() {
|
model.visible_products = ids;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
Msg::ProductFetched(NetRes::Error(_)) | Msg::ProductFetched(NetRes::Http(_)) => {
|
ListingMsg::ProductsFetched(NetRes::Error(_)) | ListingMsg::ProductsFetched(NetRes::Http(_)) => {
|
||||||
model.errors.push("Failed to load products".into());
|
model.errors.push("Failed to load products".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,15 +93,16 @@ pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<crate:
|
|||||||
|
|
||||||
pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
||||||
let products: Vec<Node<crate::Msg>> = if page.visible_products.is_empty() {
|
let products: Vec<Node<crate::Msg>> = if page.visible_products.is_empty() {
|
||||||
page.product_ids
|
page.products
|
||||||
|
.product_ids
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|id| page.products.get(id))
|
.filter_map(|id| page.products.products.get(id))
|
||||||
.map(|p| product(model, p))
|
.map(|p| product(model, p))
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
page.visible_products
|
page.visible_products
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|id| page.products.get(id))
|
.filter_map(|id| page.products.products.get(id))
|
||||||
.map(|p| product(model, p))
|
.map(|p| product(model, p))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
@ -136,8 +114,8 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
|||||||
.map_msg(Into::into);
|
.map_msg(Into::into);
|
||||||
|
|
||||||
div![
|
div![
|
||||||
crate::shared::view::public_navbar(model),
|
crate::shared::view::public_navbar::view(model, &page.products),
|
||||||
super::layout::view(model, content, Some(&page.categories))
|
super::layout::view(model, content, Some(&page.products.categories))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,54 +4,60 @@ use seed::*;
|
|||||||
use crate::api::NetRes;
|
use crate::api::NetRes;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum ProductMsg {
|
||||||
ProductFetched(NetRes<model::api::Product>),
|
ProductsFetched(NetRes<model::api::Products>),
|
||||||
SelectImage(usize),
|
SelectImage(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ProductPage {
|
pub struct ProductPage {
|
||||||
pub product_id: Option<model::ProductId>,
|
pub product_id: Option<model::ProductId>,
|
||||||
pub product: Option<model::api::Product>,
|
pub products: crate::model::Products,
|
||||||
pub selected_image: usize,
|
pub selected_image: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>) -> ProductPage {
|
pub fn init(mut url: Url, orders: &mut impl Orders<crate::Msg>) -> ProductPage {
|
||||||
let product_id = match url.remaining_path_parts().as_slice() {
|
let product_id = match url.remaining_path_parts().as_slice() {
|
||||||
["product", id] => id.parse::<model::RecordId>().unwrap_or_default().into(),
|
["product", id] => id.parse::<model::RecordId>().unwrap_or_default().into(),
|
||||||
_ => return ProductPage::default(),
|
_ => return ProductPage::default(),
|
||||||
};
|
};
|
||||||
orders.perform_cmd(async move {
|
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 {
|
ProductPage {
|
||||||
product_id: Some(product_id),
|
product_id: Some(product_id),
|
||||||
product: None,
|
products: Default::default(),
|
||||||
selected_image: 0,
|
selected_image: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_changed(_url: Url, _model: &mut ProductPage) {}
|
pub fn page_changed(_url: Url, _model: &mut ProductPage) {}
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut ProductPage, _orders: &mut impl Orders<crate::Msg>) {
|
pub fn update(msg: ProductMsg, model: &mut ProductPage, _orders: &mut impl Orders<crate::Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::ProductFetched(NetRes::Success(product)) => {
|
ProductMsg::ProductsFetched(NetRes::Success(products)) => {
|
||||||
model.product = Some(product);
|
model.products.update(products.0);
|
||||||
}
|
}
|
||||||
Msg::ProductFetched(NetRes::Error(e)) => {
|
ProductMsg::ProductsFetched(NetRes::Error(e)) => {
|
||||||
seed::error!(e);
|
seed::error!(e);
|
||||||
}
|
}
|
||||||
Msg::ProductFetched(NetRes::Http(e)) => {
|
ProductMsg::ProductsFetched(NetRes::Http(e)) => {
|
||||||
seed::error!(e);
|
seed::error!(e);
|
||||||
}
|
}
|
||||||
Msg::SelectImage(selected) => {
|
ProductMsg::SelectImage(selected) => {
|
||||||
model.selected_image = selected;
|
model.selected_image = selected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
||||||
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!(),
|
None => return empty!(),
|
||||||
Some(product) => product,
|
Some(product) => product,
|
||||||
};
|
};
|
||||||
@ -115,12 +121,12 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
|||||||
].map_msg(Into::into);
|
].map_msg(Into::into);
|
||||||
|
|
||||||
div![
|
div![
|
||||||
crate::shared::view::public_navbar(model),
|
crate::shared::view::public_navbar::view(model, &page.products),
|
||||||
super::layout::view(model, content, None)
|
super::layout::view(model, content, Some(&page.products.categories))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_section(product: &model::api::Product, model: &crate::Model) -> Node<Msg> {
|
fn action_section(product: &model::api::Product, model: &crate::Model) -> Node<ProductMsg> {
|
||||||
if product.available {
|
if product.available {
|
||||||
div![
|
div![
|
||||||
C!["flex py-4 space-x-4"],
|
C!["flex py-4 space-x-4"],
|
||||||
@ -139,7 +145,7 @@ fn action_section(product: &model::api::Product, model: &crate::Model) -> Node<M
|
|||||||
ev("click", move |ev| {
|
ev("click", move |ev| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
None as Option<Msg>
|
None as Option<ProductMsg>
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -148,7 +154,7 @@ fn action_section(product: &model::api::Product, model: &crate::Model) -> Node<M
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<Msg> {
|
fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<ProductMsg> {
|
||||||
match product.deliver_days_flag.len() {
|
match product.deliver_days_flag.len() {
|
||||||
0 => return empty![],
|
0 => return empty![],
|
||||||
7 => return div![model.i18n.t("Delivery all week")],
|
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<Msg> {
|
fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node<ProductMsg> {
|
||||||
div![
|
div![
|
||||||
C!["flex-1 px-2"],
|
C!["flex-1 px-2"],
|
||||||
button![
|
button![
|
||||||
@ -180,13 +186,13 @@ fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node<Msg
|
|||||||
ev("click", move |ev| {
|
ev("click", move |ev| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
Msg::SelectImage(idx)
|
ProductMsg::SelectImage(idx)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image(img: &model::api::Photo) -> Node<Msg> {
|
fn image(img: &model::api::Photo) -> Node<ProductMsg> {
|
||||||
div![
|
div![
|
||||||
C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4 flex items-center justify-center"],
|
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()]]
|
img![C!["h-64 md:h-80"], attrs!["src" => img.url.as_str()]]
|
||||||
|
177
web/src/pages/public/shopping_cart.rs
Normal file
177
web/src/pages/public/shopping_cart.rs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
use rusty_money::Money;
|
||||||
|
use seed::prelude::*;
|
||||||
|
use seed::*;
|
||||||
|
|
||||||
|
use crate::api::NetRes;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ShoppingCartMsg {
|
||||||
|
ProductsFetched(NetRes<model::api::Products>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ShoppingCartPage {
|
||||||
|
pub products: crate::model::Products,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(_url: Url, orders: &mut impl Orders<crate::Msg>) -> 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<crate::Msg>,
|
||||||
|
) {
|
||||||
|
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<crate::Msg> {
|
||||||
|
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<crate::Msg> {
|
||||||
|
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<crate::Msg> {
|
||||||
|
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<crate::Msg> {
|
||||||
|
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<crate::Msg> {
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
@ -4,7 +4,7 @@ use seed::*;
|
|||||||
use crate::pages::Urls;
|
use crate::pages::Urls;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum LogInMsg {
|
||||||
LoginChanged(String),
|
LoginChanged(String),
|
||||||
PasswordChanged(String),
|
PasswordChanged(String),
|
||||||
Submit,
|
Submit,
|
||||||
@ -16,7 +16,7 @@ pub struct SignInPage {
|
|||||||
pub password: model::Password,
|
pub password: model::Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignInPage {
|
pub fn init(mut _url: Url, _orders: &mut impl Orders<LogInMsg>) -> SignInPage {
|
||||||
SignInPage {
|
SignInPage {
|
||||||
login: model::Login::new(""),
|
login: model::Login::new(""),
|
||||||
password: model::Password::new(""),
|
password: model::Password::new(""),
|
||||||
@ -25,7 +25,7 @@ pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignInPage {
|
|||||||
|
|
||||||
pub fn page_changed(_url: Url, _model: &mut SignInPage) {}
|
pub fn page_changed(_url: Url, _model: &mut SignInPage) {}
|
||||||
|
|
||||||
pub fn update(_msg: Msg, _model: &mut SignInPage, _orders: &mut impl Orders<crate::Msg>) {}
|
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> {
|
pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
|
||||||
let home = Urls::new(&model.url).home();
|
let home = Urls::new(&model.url).home();
|
||||||
@ -66,13 +66,13 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
|
|||||||
div![super::layout::view(model, content, None)]
|
div![super::layout::view(model, content, None)]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<Msg> {
|
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<LogInMsg> {
|
||||||
form![
|
form![
|
||||||
C!["mt-6"],
|
C!["mt-6"],
|
||||||
ev("submit", |ev| {
|
ev("submit", |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
Msg::Submit
|
LogInMsg::Submit
|
||||||
}),
|
}),
|
||||||
div![
|
div![
|
||||||
label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")],
|
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<Msg> {
|
|||||||
ev(Ev::Change, |ev| {
|
ev(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
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<Msg> {
|
|||||||
ev(Ev::Change, |ev| {
|
ev(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
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![
|
a![
|
||||||
|
@ -7,7 +7,7 @@ use seed::*;
|
|||||||
use crate::pages::Urls;
|
use crate::pages::Urls;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Msg {
|
pub enum RegisterMsg {
|
||||||
LoginChanged(String),
|
LoginChanged(String),
|
||||||
EmailChanged(String),
|
EmailChanged(String),
|
||||||
PasswordChanged(String),
|
PasswordChanged(String),
|
||||||
@ -24,7 +24,7 @@ pub struct SignUpPage {
|
|||||||
pub password_confirmation: model::PasswordConfirmation,
|
pub password_confirmation: model::PasswordConfirmation,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
pub fn init(mut _url: Url, _orders: &mut impl Orders<RegisterMsg>) -> SignUpPage {
|
||||||
SignUpPage {
|
SignUpPage {
|
||||||
login: model::Login::new(""),
|
login: model::Login::new(""),
|
||||||
email: model::Email::invalid_empty(),
|
email: model::Email::invalid_empty(),
|
||||||
@ -35,23 +35,23 @@ pub fn init(mut _url: Url, _orders: &mut impl Orders<Msg>) -> SignUpPage {
|
|||||||
|
|
||||||
pub fn page_changed(_url: Url, _model: &mut SignUpPage) {}
|
pub fn page_changed(_url: Url, _model: &mut SignUpPage) {}
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut SignUpPage, orders: &mut impl Orders<crate::Msg>) {
|
pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders<crate::Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::LoginChanged(value) => {
|
RegisterMsg::LoginChanged(value) => {
|
||||||
model.login = Login::new(value);
|
model.login = Login::new(value);
|
||||||
}
|
}
|
||||||
Msg::EmailChanged(value) => {
|
RegisterMsg::EmailChanged(value) => {
|
||||||
if let Ok(email) = Email::from_str(&value) {
|
if let Ok(email) = Email::from_str(&value) {
|
||||||
model.email = email;
|
model.email = email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::PasswordChanged(value) => {
|
RegisterMsg::PasswordChanged(value) => {
|
||||||
model.password = Password::new(value);
|
model.password = Password::new(value);
|
||||||
}
|
}
|
||||||
Msg::PasswordConfirmationChanged(value) => {
|
RegisterMsg::PasswordConfirmationChanged(value) => {
|
||||||
model.password_confirmation = PasswordConfirmation::new(value);
|
model.password_confirmation = PasswordConfirmation::new(value);
|
||||||
}
|
}
|
||||||
Msg::Submit => {
|
RegisterMsg::Submit => {
|
||||||
let email = model.email.clone();
|
let email = model.email.clone();
|
||||||
let login = model.login.clone();
|
let login = model.login.clone();
|
||||||
let password = model.password.clone();
|
let password = model.password.clone();
|
||||||
@ -59,7 +59,7 @@ pub fn update(msg: Msg, model: &mut SignUpPage, orders: &mut impl Orders<crate::
|
|||||||
|
|
||||||
orders.perform_cmd(async move {
|
orders.perform_cmd(async move {
|
||||||
crate::Msg::Public(
|
crate::Msg::Public(
|
||||||
Msg::AccountCreated(
|
RegisterMsg::AccountCreated(
|
||||||
crate::api::public::sign_up(model::api::CreateAccountInput {
|
crate::api::public::sign_up(model::api::CreateAccountInput {
|
||||||
email,
|
email,
|
||||||
login,
|
login,
|
||||||
@ -72,7 +72,7 @@ pub fn update(msg: Msg, model: &mut SignUpPage, orders: &mut impl Orders<crate::
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Msg::AccountCreated(_) => {}
|
RegisterMsg::AccountCreated(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,13 +114,13 @@ pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> {
|
|||||||
div![super::layout::view(model, content, None)]
|
div![super::layout::view(model, content, None)]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<RegisterMsg> {
|
||||||
form![
|
form![
|
||||||
C!["mt-6"],
|
C!["mt-6"],
|
||||||
ev("submit", |ev| {
|
ev("submit", |ev| {
|
||||||
ev.stop_propagation();
|
ev.stop_propagation();
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
Msg::Submit
|
RegisterMsg::Submit
|
||||||
}),
|
}),
|
||||||
div![
|
div![
|
||||||
label![attrs!["for" => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")],
|
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<Msg> {
|
|||||||
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"],
|
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(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
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<Msg> {
|
|||||||
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"],
|
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(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
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<Msg> {
|
|||||||
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"]],
|
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(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
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![
|
div![
|
||||||
@ -166,7 +166,7 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
|||||||
],
|
],
|
||||||
ev(Ev::Change, |ev| {
|
ev(Ev::Change, |ev| {
|
||||||
ev.stop_propagation();
|
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![
|
div![
|
||||||
|
@ -4,7 +4,7 @@ use seed::prelude::*;
|
|||||||
|
|
||||||
use crate::pages::AdminPage;
|
use crate::pages::AdminPage;
|
||||||
use crate::shared::notification::NotificationMsg;
|
use crate::shared::notification::NotificationMsg;
|
||||||
use crate::{pages, Model, Msg, Page, PublicPage};
|
use crate::{Model, Msg, Page, PublicPage};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SessionMsg {
|
pub enum SessionMsg {
|
||||||
@ -60,7 +60,7 @@ pub fn redirect_on_session(model: &Model, orders: &mut impl Orders<Msg>) {
|
|||||||
PublicPage::Product(_) => {}
|
PublicPage::Product(_) => {}
|
||||||
PublicPage::SignIn(_) => {}
|
PublicPage::SignIn(_) => {}
|
||||||
PublicPage::SignUp(_) => {}
|
PublicPage::SignUp(_) => {}
|
||||||
PublicPage::ShoppingCart => {}
|
PublicPage::ShoppingCart(_) => {}
|
||||||
PublicPage::Checkout => {}
|
PublicPage::Checkout => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use seed::app::Orders;
|
use seed::app::Orders;
|
||||||
|
|
||||||
|
use crate::api::NetRes;
|
||||||
use crate::session::redirect_on_session;
|
use crate::session::redirect_on_session;
|
||||||
|
use crate::shared::notification::NotificationMsg;
|
||||||
|
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
@ -8,10 +10,16 @@ pub mod view;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SharedMsg {
|
pub enum SharedMsg {
|
||||||
LoadMe,
|
LoadMe,
|
||||||
MeLoaded(crate::api::NetRes<model::Account>),
|
MeLoaded(NetRes<model::Account>),
|
||||||
SignIn(model::api::SignInInput),
|
SignIn(model::api::SignInInput),
|
||||||
SignedIn(crate::api::NetRes<model::api::SessionOutput>),
|
SignedIn(NetRes<model::api::SessionOutput>),
|
||||||
Notification(notification::NotificationMsg),
|
Notification(NotificationMsg),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SharedMsg> for crate::Msg {
|
||||||
|
fn from(msg: SharedMsg) -> Self {
|
||||||
|
Self::Shared(msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[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);
|
model.shared.me = Some(account);
|
||||||
redirect_on_session(model, orders);
|
redirect_on_session(model, orders);
|
||||||
}
|
}
|
||||||
SharedMsg::MeLoaded(crate::api::NetRes::Error(_error)) => {}
|
SharedMsg::MeLoaded(NetRes::Error(_error)) => {}
|
||||||
SharedMsg::MeLoaded(crate::api::NetRes::Http(_error)) => {}
|
SharedMsg::MeLoaded(NetRes::Http(_error)) => {}
|
||||||
SharedMsg::SignIn(input) => {
|
SharedMsg::SignIn(input) => {
|
||||||
orders.skip().perform_cmd(async {
|
orders.skip().perform_cmd(async {
|
||||||
SharedMsg::SignedIn(crate::api::public::sign_in(input).await)
|
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);
|
handle_auth_pair(pair, &mut model.shared, orders);
|
||||||
}
|
}
|
||||||
SharedMsg::SignedIn(crate::api::NetRes::Error(_err)) => {}
|
SharedMsg::SignedIn(NetRes::Error(_err)) => {}
|
||||||
SharedMsg::SignedIn(crate::api::NetRes::Http(_err)) => {}
|
SharedMsg::SignedIn(NetRes::Http(_err)) => {}
|
||||||
SharedMsg::Notification(msg) => {
|
SharedMsg::Notification(msg) => {
|
||||||
notification::update(msg, &mut model.shared, orders);
|
notification::update(msg, &mut model.shared, orders);
|
||||||
}
|
}
|
||||||
|
@ -1,78 +1,233 @@
|
|||||||
use seed::prelude::*;
|
pub mod public_navbar {
|
||||||
use seed::*;
|
use seed::prelude::*;
|
||||||
|
use seed::*;
|
||||||
|
|
||||||
use crate::pages::Urls;
|
use crate::pages::Urls;
|
||||||
use crate::Msg;
|
use crate::shopping_cart::CartMsg;
|
||||||
|
use crate::Msg;
|
||||||
|
|
||||||
pub fn public_navbar(model: &crate::Model) -> Node<Msg> {
|
pub fn view(model: &crate::Model, products: &crate::model::Products) -> Node<Msg> {
|
||||||
header![
|
header![
|
||||||
C!["container flex justify-around py-8 mx-auto bg-white"],
|
C!["container flex justify-around py-8 mx-auto bg-white"],
|
||||||
div![C!["flex items-center"], logo(model)],
|
div![C!["flex items-center"], logo(model)],
|
||||||
div![
|
div![
|
||||||
C!["items-center hidden space-x-8 lg:flex"],
|
C!["items-center hidden space-x-8 lg:flex"],
|
||||||
navbar_item(div![C![""], "Home"], Urls::new(model.url.clone()).home()),
|
navbar_item(div![C![""], "Home"], Urls::new(&model.url).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<Msg>, path: Url) -> Node<Msg> {
|
|
||||||
a![
|
|
||||||
attrs!["href" => path],
|
|
||||||
C!["px-4 py-2 font-semibold text-gray-600 rounded"],
|
|
||||||
name
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn logo(model: &crate::Model) -> Node<Msg> {
|
|
||||||
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;"]
|
|
||||||
],
|
],
|
||||||
_ => 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::<i32>()
|
||||||
|
],
|
||||||
|
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<Msg>, path: Url) -> Node<Msg> {
|
||||||
|
a![
|
||||||
|
attrs!["href" => path],
|
||||||
|
C!["px-4 py-2 font-semibold text-gray-600 rounded"],
|
||||||
|
name
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logo(model: &crate::Model) -> Node<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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<Msg> {
|
pub mod cart_dropdown {
|
||||||
svg![
|
use model::ProductId;
|
||||||
attrs![
|
use seed::prelude::*;
|
||||||
"width" => "32px",
|
use seed::*;
|
||||||
"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"]]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn account() -> Node<Msg> {
|
use crate::shopping_cart::Item;
|
||||||
svg![
|
use crate::{Model, Msg};
|
||||||
attrs![
|
|
||||||
"xmlns" => "http://www.w3.org/2000/svg",
|
macro_rules! filter_products {
|
||||||
"class" => "w-6 h-6",
|
($model: expr, $products: expr) => {
|
||||||
"fill" => "none",
|
$model
|
||||||
"viewBox" => "0 0 24 24",
|
.cart
|
||||||
"stroke" => "currentColor",
|
.items
|
||||||
],
|
.values()
|
||||||
path![attrs![
|
.filter_map(|item: &Item| filter_product(item, $products, item.product_id))
|
||||||
"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"
|
pub fn view(model: &Model, products: &crate::model::Products) -> Node<Msg> {
|
||||||
]],
|
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<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ pub enum CartMsg {
|
|||||||
quantity_unit: QuantityUnit,
|
quantity_unit: QuantityUnit,
|
||||||
product_id: ProductId,
|
product_id: ProductId,
|
||||||
},
|
},
|
||||||
|
Hover,
|
||||||
|
Leave,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CartMsg> for Msg {
|
impl From<CartMsg> for Msg {
|
||||||
@ -28,15 +30,17 @@ pub type Items = indexmap::IndexMap<ProductId, Item>;
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
product_id: ProductId,
|
pub product_id: ProductId,
|
||||||
quantity: Quantity,
|
pub quantity: Quantity,
|
||||||
quantity_unit: QuantityUnit,
|
pub quantity_unit: QuantityUnit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct ShoppingCart {
|
pub struct ShoppingCart {
|
||||||
pub cart_id: Option<model::ShoppingCartId>,
|
pub cart_id: Option<model::ShoppingCartId>,
|
||||||
pub items: Items,
|
pub items: Items,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub hover: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(model: &mut Model, _orders: &mut impl Orders<Msg>) {
|
pub fn init(model: &mut Model, _orders: &mut impl Orders<Msg>) {
|
||||||
@ -52,22 +56,34 @@ 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 = items.entry(product_id).or_insert_with(|| Item {
|
let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item {
|
||||||
quantity,
|
quantity,
|
||||||
quantity_unit,
|
quantity_unit,
|
||||||
product_id,
|
product_id,
|
||||||
});
|
});
|
||||||
entry.quantity = quantity;
|
entry.quantity = entry.quantity + quantity;
|
||||||
entry.quantity_unit = quantity_unit;
|
entry.quantity_unit = quantity_unit;
|
||||||
}
|
}
|
||||||
store_local(&model.cart);
|
store_local(&model.cart);
|
||||||
}
|
}
|
||||||
CartMsg::ModifyItem { .. } => {}
|
CartMsg::ModifyItem { .. } => {}
|
||||||
|
CartMsg::Hover => {
|
||||||
|
model.cart.hover = true;
|
||||||
|
}
|
||||||
|
CartMsg::Leave => {
|
||||||
|
model.cart.hover = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_local() -> ShoppingCart {
|
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) {
|
fn store_local(cart: &ShoppingCart) {
|
||||||
LocalStorage::insert("ct", cart).ok();
|
LocalStorage::insert("ct", cart).ok();
|
||||||
|
Loading…
Reference in New Issue
Block a user