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",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -2055,26 +2053,10 @@ checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libssh2-sys",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"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]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.6"
|
||||
@ -2225,15 +2207,6 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minidom"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@ -2802,15 +2775,6 @@ version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
@ -3200,42 +3164,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
@ -4358,8 +4286,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"model",
|
||||
"rusty-money",
|
||||
"seed 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"seed_heroicons",
|
||||
"seed",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
|
@ -7,6 +7,7 @@ mod dummy;
|
||||
pub mod encrypt;
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{Deref, DerefMut, Display, From};
|
||||
@ -276,6 +277,14 @@ impl Default for Audience {
|
||||
#[serde(transparent)]
|
||||
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 = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(transparent))]
|
||||
@ -283,6 +292,22 @@ pub struct Price(NonNegative);
|
||||
#[serde(transparent)]
|
||||
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 {
|
||||
type Error = TransformError;
|
||||
|
||||
@ -366,6 +391,22 @@ impl<'de> serde::Deserialize<'de> for Email {
|
||||
#[serde(transparent)]
|
||||
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 {
|
||||
type Error = TransformError;
|
||||
|
||||
|
@ -10,7 +10,7 @@ crate-type = ["cdylib"]
|
||||
model = { path = "../shared/model", features = ["dummy"] }
|
||||
|
||||
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"] }
|
||||
gloo-timers = { version = "*", features = ["futures"] }
|
||||
|
@ -35,5 +35,10 @@ pub fn define(i18n: &mut I18n) {
|
||||
"There was internal server error. Please try later",
|
||||
"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(),
|
||||
i18n: I18n::load(),
|
||||
cart: Default::default(),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
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);
|
||||
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
|
||||
}
|
||||
Msg::Public(pages::public::Msg::SignUp(pages::public::sign_up::Msg::AccountCreated(
|
||||
res,
|
||||
))) => {
|
||||
Msg::Public(pages::public::Msg::SignUp(
|
||||
pages::public::sign_up::RegisterMsg::AccountCreated(res),
|
||||
)) => {
|
||||
orders
|
||||
.skip()
|
||||
.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);
|
||||
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::Session(msg) => {
|
||||
session::update(msg, model, orders);
|
||||
}
|
||||
Msg::Cart(msg) => {
|
||||
shopping_cart::update(msg, model, orders);
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
Msg::Debug(msg) => {
|
||||
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::SignIn(page)) => pages::public::sign_in::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![],
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use seed::Url;
|
||||
|
||||
use crate::{I18n, Page, shopping_cart};
|
||||
use crate::{shopping_cart, I18n, Page};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
@ -11,6 +13,51 @@ pub struct Model {
|
||||
pub shared: crate::shared::Model,
|
||||
pub i18n: I18n,
|
||||
pub cart: shopping_cart::ShoppingCart,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
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),
|
||||
SignIn(public::sign_in::SignInPage),
|
||||
SignUp(public::sign_up::SignUpPage),
|
||||
ShoppingCart,
|
||||
ShoppingCart(public::shopping_cart::ShoppingCartPage),
|
||||
Checkout,
|
||||
}
|
||||
|
||||
@ -59,6 +59,9 @@ impl Page {
|
||||
url,
|
||||
&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(
|
||||
url,
|
||||
&mut orders.proxy(Into::into),
|
||||
@ -89,6 +92,11 @@ impl Page {
|
||||
let page = crate::fetch_page!(public page self, Product, Page::init(url, orders));
|
||||
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", ..] => {
|
||||
let page = crate::fetch_page!(public page self, SignIn, Page::init(url, orders));
|
||||
public::sign_in::page_changed(url, page);
|
||||
@ -131,6 +139,10 @@ impl<'a> Urls<'a> {
|
||||
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 {
|
||||
self.base_url().add_path_part("sign-up")
|
||||
}
|
||||
|
@ -1,64 +1,78 @@
|
||||
pub mod listing;
|
||||
pub mod product;
|
||||
pub mod shopping_cart;
|
||||
pub mod sign_in;
|
||||
pub(crate) mod sign_up;
|
||||
pub mod sign_up;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
Listing(listing::Msg),
|
||||
Product(product::Msg),
|
||||
SignIn(sign_in::Msg),
|
||||
SignUp(sign_up::Msg),
|
||||
Listing(listing::ListingMsg),
|
||||
Product(product::ProductMsg),
|
||||
SignIn(sign_in::LogInMsg),
|
||||
SignUp(sign_up::RegisterMsg),
|
||||
ShoppingCart(shopping_cart::ShoppingCartMsg),
|
||||
}
|
||||
|
||||
impl From<listing::Msg> for Msg {
|
||||
fn from(msg: listing::Msg) -> Self {
|
||||
impl From<listing::ListingMsg> for Msg {
|
||||
fn from(msg: listing::ListingMsg) -> Self {
|
||||
Self::Listing(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<product::Msg> for Msg {
|
||||
fn from(msg: product::Msg) -> Self {
|
||||
impl From<product::ProductMsg> for Msg {
|
||||
fn from(msg: product::ProductMsg) -> Self {
|
||||
Self::Product(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_in::Msg> for Msg {
|
||||
fn from(msg: sign_in::Msg) -> Self {
|
||||
impl From<sign_in::LogInMsg> for Msg {
|
||||
fn from(msg: sign_in::LogInMsg) -> Self {
|
||||
Self::SignIn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_up::Msg> for Msg {
|
||||
fn from(msg: sign_up::Msg) -> Self {
|
||||
impl From<sign_up::RegisterMsg> for Msg {
|
||||
fn from(msg: sign_up::RegisterMsg) -> Self {
|
||||
Self::SignUp(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<listing::Msg> for crate::Msg {
|
||||
fn from(msg: listing::Msg) -> Self {
|
||||
impl From<shopping_cart::ShoppingCartMsg> for Msg {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<product::Msg> for crate::Msg {
|
||||
fn from(msg: product::Msg) -> Self {
|
||||
impl From<product::ProductMsg> for crate::Msg {
|
||||
fn from(msg: product::ProductMsg) -> Self {
|
||||
crate::Msg::Public(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_in::Msg> for crate::Msg {
|
||||
fn from(msg: sign_in::Msg) -> Self {
|
||||
impl From<sign_in::LogInMsg> for crate::Msg {
|
||||
fn from(msg: sign_in::LogInMsg) -> Self {
|
||||
crate::Msg::Public(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign_up::Msg> for crate::Msg {
|
||||
fn from(msg: sign_up::Msg) -> Self {
|
||||
impl From<sign_up::RegisterMsg> for crate::Msg {
|
||||
fn from(msg: sign_up::RegisterMsg) -> Self {
|
||||
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 {
|
||||
fn from(msg: Msg) -> Self {
|
||||
crate::Msg::Public(msg)
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use model::Quantity;
|
||||
use seed::app::Orders;
|
||||
@ -6,33 +6,31 @@ use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::api::NetRes;
|
||||
use crate::model::Products;
|
||||
use crate::pages::Urls;
|
||||
use crate::shopping_cart::CartMsg;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ListingPage {
|
||||
pub product_ids: Vec<model::ProductId>,
|
||||
pub products: HashMap<model::ProductId, model::api::Product>,
|
||||
pub errors: Vec<String>,
|
||||
pub categories: Vec<model::api::Category>,
|
||||
pub filters: HashSet<String>,
|
||||
pub visible_products: Vec<model::ProductId>,
|
||||
|
||||
pub products: Products,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
pub enum ListingMsg {
|
||||
FetchProducts,
|
||||
ProductFetched(NetRes<model::api::Products>),
|
||||
ProductsFetched(NetRes<model::api::Products>),
|
||||
}
|
||||
|
||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> ListingPage {
|
||||
orders.send_msg(Msg::FetchProducts);
|
||||
pub fn init(url: Url, orders: &mut impl Orders<ListingMsg>) -> ListingPage {
|
||||
orders.send_msg(ListingMsg::FetchProducts);
|
||||
ListingPage {
|
||||
product_ids: vec![],
|
||||
products: Products::default(),
|
||||
filters: url_to_filters(url),
|
||||
products: Default::default(),
|
||||
errors: vec![],
|
||||
categories: 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) {
|
||||
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) {
|
||||
model.visible_products = model
|
||||
.product_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
model.products.get(id).and_then(|p| {
|
||||
p.category
|
||||
.as_ref()
|
||||
.filter(|c| model.filters.contains(c.key.as_str()))
|
||||
.map(|_| p.id)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
fn filter_product(model: &ListingPage, product: &model::api::Product) -> bool {
|
||||
product
|
||||
.category
|
||||
.as_ref()
|
||||
.filter(|c| model.filters.contains(c.key.as_str()))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
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 {
|
||||
Msg::FetchProducts => {
|
||||
ListingMsg::FetchProducts => {
|
||||
orders.skip().perform_cmd({
|
||||
async {
|
||||
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)) => {
|
||||
model.categories = products
|
||||
.0
|
||||
.iter()
|
||||
.fold(HashSet::new(), |mut set, p| {
|
||||
if let Some(category) = p.category.as_ref().cloned() {
|
||||
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);
|
||||
ListingMsg::ProductsFetched(NetRes::Success(products)) => {
|
||||
model.products.update(products.0);
|
||||
let ids = model
|
||||
.products
|
||||
.filter_product_ids(|product| filter_product(model, product));
|
||||
model.visible_products = ids;
|
||||
}
|
||||
Msg::ProductFetched(NetRes::Error(_)) | Msg::ProductFetched(NetRes::Http(_)) => {
|
||||
ListingMsg::ProductsFetched(NetRes::Error(_)) | ListingMsg::ProductsFetched(NetRes::Http(_)) => {
|
||||
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> {
|
||||
let products: Vec<Node<crate::Msg>> = if page.visible_products.is_empty() {
|
||||
page.product_ids
|
||||
page.products
|
||||
.product_ids
|
||||
.iter()
|
||||
.filter_map(|id| page.products.get(id))
|
||||
.filter_map(|id| page.products.products.get(id))
|
||||
.map(|p| product(model, p))
|
||||
.collect()
|
||||
} else {
|
||||
page.visible_products
|
||||
.iter()
|
||||
.filter_map(|id| page.products.get(id))
|
||||
.filter_map(|id| page.products.products.get(id))
|
||||
.map(|p| product(model, p))
|
||||
.collect()
|
||||
};
|
||||
@ -136,8 +114,8 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
||||
.map_msg(Into::into);
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(model, content, Some(&page.categories))
|
||||
crate::shared::view::public_navbar::view(model, &page.products),
|
||||
super::layout::view(model, content, Some(&page.products.categories))
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -4,54 +4,60 @@ use seed::*;
|
||||
use crate::api::NetRes;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
ProductFetched(NetRes<model::api::Product>),
|
||||
pub enum ProductMsg {
|
||||
ProductsFetched(NetRes<model::api::Products>),
|
||||
SelectImage(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProductPage {
|
||||
pub product_id: Option<model::ProductId>,
|
||||
pub product: Option<model::api::Product>,
|
||||
pub products: crate::model::Products,
|
||||
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() {
|
||||
["product", id] => id.parse::<model::RecordId>().unwrap_or_default().into(),
|
||||
_ => return ProductPage::default(),
|
||||
};
|
||||
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 {
|
||||
product_id: Some(product_id),
|
||||
product: None,
|
||||
products: Default::default(),
|
||||
selected_image: 0,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Msg::ProductFetched(NetRes::Success(product)) => {
|
||||
model.product = Some(product);
|
||||
ProductMsg::ProductsFetched(NetRes::Success(products)) => {
|
||||
model.products.update(products.0);
|
||||
}
|
||||
Msg::ProductFetched(NetRes::Error(e)) => {
|
||||
ProductMsg::ProductsFetched(NetRes::Error(e)) => {
|
||||
seed::error!(e);
|
||||
}
|
||||
Msg::ProductFetched(NetRes::Http(e)) => {
|
||||
ProductMsg::ProductsFetched(NetRes::Http(e)) => {
|
||||
seed::error!(e);
|
||||
}
|
||||
Msg::SelectImage(selected) => {
|
||||
ProductMsg::SelectImage(selected) => {
|
||||
model.selected_image = selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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!(),
|
||||
Some(product) => product,
|
||||
};
|
||||
@ -115,12 +121,12 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
||||
].map_msg(Into::into);
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(model, content, None)
|
||||
crate::shared::view::public_navbar::view(model, &page.products),
|
||||
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 {
|
||||
div![
|
||||
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.prevent_default();
|
||||
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() {
|
||||
0 => return empty![],
|
||||
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![
|
||||
C!["flex-1 px-2"],
|
||||
button![
|
||||
@ -180,13 +186,13 @@ fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node<Msg
|
||||
ev("click", move |ev| {
|
||||
ev.prevent_default();
|
||||
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![
|
||||
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()]]
|
||||
|
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;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
pub enum LogInMsg {
|
||||
LoginChanged(String),
|
||||
PasswordChanged(String),
|
||||
Submit,
|
||||
@ -16,7 +16,7 @@ pub struct SignInPage {
|
||||
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 {
|
||||
login: model::Login::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 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> {
|
||||
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)]
|
||||
}
|
||||
|
||||
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<Msg> {
|
||||
fn sign_in_form(model: &crate::Model, _page: &SignInPage) -> Node<LogInMsg> {
|
||||
form![
|
||||
C!["mt-6"],
|
||||
ev("submit", |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::Submit
|
||||
LogInMsg::Submit
|
||||
}),
|
||||
div![
|
||||
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.stop_propagation();
|
||||
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.stop_propagation();
|
||||
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![
|
||||
|
@ -7,7 +7,7 @@ use seed::*;
|
||||
use crate::pages::Urls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
pub enum RegisterMsg {
|
||||
LoginChanged(String),
|
||||
EmailChanged(String),
|
||||
PasswordChanged(String),
|
||||
@ -24,7 +24,7 @@ pub struct SignUpPage {
|
||||
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 {
|
||||
login: model::Login::new(""),
|
||||
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 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 {
|
||||
Msg::LoginChanged(value) => {
|
||||
RegisterMsg::LoginChanged(value) => {
|
||||
model.login = Login::new(value);
|
||||
}
|
||||
Msg::EmailChanged(value) => {
|
||||
RegisterMsg::EmailChanged(value) => {
|
||||
if let Ok(email) = Email::from_str(&value) {
|
||||
model.email = email;
|
||||
}
|
||||
}
|
||||
Msg::PasswordChanged(value) => {
|
||||
RegisterMsg::PasswordChanged(value) => {
|
||||
model.password = Password::new(value);
|
||||
}
|
||||
Msg::PasswordConfirmationChanged(value) => {
|
||||
RegisterMsg::PasswordConfirmationChanged(value) => {
|
||||
model.password_confirmation = PasswordConfirmation::new(value);
|
||||
}
|
||||
Msg::Submit => {
|
||||
RegisterMsg::Submit => {
|
||||
let email = model.email.clone();
|
||||
let login = model.login.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 {
|
||||
crate::Msg::Public(
|
||||
Msg::AccountCreated(
|
||||
RegisterMsg::AccountCreated(
|
||||
crate::api::public::sign_up(model::api::CreateAccountInput {
|
||||
email,
|
||||
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)]
|
||||
}
|
||||
|
||||
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<RegisterMsg> {
|
||||
form![
|
||||
C!["mt-6"],
|
||||
ev("submit", |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::Submit
|
||||
RegisterMsg::Submit
|
||||
}),
|
||||
div![
|
||||
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"],
|
||||
ev(Ev::Change, |ev| {
|
||||
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"],
|
||||
ev(Ev::Change, |ev| {
|
||||
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"]],
|
||||
ev(Ev::Change, |ev| {
|
||||
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![
|
||||
@ -166,7 +166,7 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<Msg> {
|
||||
],
|
||||
ev(Ev::Change, |ev| {
|
||||
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![
|
||||
|
@ -4,7 +4,7 @@ use seed::prelude::*;
|
||||
|
||||
use crate::pages::AdminPage;
|
||||
use crate::shared::notification::NotificationMsg;
|
||||
use crate::{pages, Model, Msg, Page, PublicPage};
|
||||
use crate::{Model, Msg, Page, PublicPage};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionMsg {
|
||||
@ -60,7 +60,7 @@ pub fn redirect_on_session(model: &Model, orders: &mut impl Orders<Msg>) {
|
||||
PublicPage::Product(_) => {}
|
||||
PublicPage::SignIn(_) => {}
|
||||
PublicPage::SignUp(_) => {}
|
||||
PublicPage::ShoppingCart => {}
|
||||
PublicPage::ShoppingCart(_) => {}
|
||||
PublicPage::Checkout => {}
|
||||
},
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use seed::app::Orders;
|
||||
|
||||
use crate::api::NetRes;
|
||||
use crate::session::redirect_on_session;
|
||||
use crate::shared::notification::NotificationMsg;
|
||||
|
||||
pub mod notification;
|
||||
pub mod view;
|
||||
@ -8,10 +10,16 @@ pub mod view;
|
||||
#[derive(Debug)]
|
||||
pub enum SharedMsg {
|
||||
LoadMe,
|
||||
MeLoaded(crate::api::NetRes<model::Account>),
|
||||
MeLoaded(NetRes<model::Account>),
|
||||
SignIn(model::api::SignInInput),
|
||||
SignedIn(crate::api::NetRes<model::api::SessionOutput>),
|
||||
Notification(notification::NotificationMsg),
|
||||
SignedIn(NetRes<model::api::SessionOutput>),
|
||||
Notification(NotificationMsg),
|
||||
}
|
||||
|
||||
impl From<SharedMsg> for crate::Msg {
|
||||
fn from(msg: SharedMsg) -> Self {
|
||||
Self::Shared(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
redirect_on_session(model, orders);
|
||||
}
|
||||
SharedMsg::MeLoaded(crate::api::NetRes::Error(_error)) => {}
|
||||
SharedMsg::MeLoaded(crate::api::NetRes::Http(_error)) => {}
|
||||
SharedMsg::MeLoaded(NetRes::Error(_error)) => {}
|
||||
SharedMsg::MeLoaded(NetRes::Http(_error)) => {}
|
||||
SharedMsg::SignIn(input) => {
|
||||
orders.skip().perform_cmd(async {
|
||||
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);
|
||||
}
|
||||
SharedMsg::SignedIn(crate::api::NetRes::Error(_err)) => {}
|
||||
SharedMsg::SignedIn(crate::api::NetRes::Http(_err)) => {}
|
||||
SharedMsg::SignedIn(NetRes::Error(_err)) => {}
|
||||
SharedMsg::SignedIn(NetRes::Http(_err)) => {}
|
||||
SharedMsg::Notification(msg) => {
|
||||
notification::update(msg, &mut model.shared, orders);
|
||||
}
|
||||
|
@ -1,78 +1,233 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
pub mod public_navbar {
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::pages::Urls;
|
||||
use crate::Msg;
|
||||
use crate::pages::Urls;
|
||||
use crate::shopping_cart::CartMsg;
|
||||
use crate::Msg;
|
||||
|
||||
pub fn public_navbar(model: &crate::Model) -> Node<Msg> {
|
||||
header![
|
||||
C!["container flex justify-around py-8 mx-auto bg-white"],
|
||||
div![C!["flex items-center"], logo(model)],
|
||||
div![
|
||||
C!["items-center hidden space-x-8 lg:flex"],
|
||||
navbar_item(div![C![""], "Home"], Urls::new(model.url.clone()).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;"]
|
||||
pub fn view(model: &crate::Model, products: &crate::model::Products) -> Node<Msg> {
|
||||
header![
|
||||
C!["container flex justify-around py-8 mx-auto bg-white"],
|
||||
div![C!["flex items-center"], logo(model)],
|
||||
div![
|
||||
C!["items-center hidden space-x-8 lg:flex"],
|
||||
navbar_item(div![C![""], "Home"], Urls::new(&model.url).home()),
|
||||
],
|
||||
_ => 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> {
|
||||
svg![
|
||||
attrs![
|
||||
"width" => "32px",
|
||||
"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"]]
|
||||
]
|
||||
}
|
||||
pub mod cart_dropdown {
|
||||
use model::ProductId;
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
fn account() -> Node<Msg> {
|
||||
svg![
|
||||
attrs![
|
||||
"xmlns" => "http://www.w3.org/2000/svg",
|
||||
"class" => "w-6 h-6",
|
||||
"fill" => "none",
|
||||
"viewBox" => "0 0 24 24",
|
||||
"stroke" => "currentColor",
|
||||
],
|
||||
path![attrs![
|
||||
"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"
|
||||
]],
|
||||
]
|
||||
use crate::shopping_cart::Item;
|
||||
use crate::{Model, Msg};
|
||||
|
||||
macro_rules! filter_products {
|
||||
($model: expr, $products: expr) => {
|
||||
$model
|
||||
.cart
|
||||
.items
|
||||
.values()
|
||||
.filter_map(|item: &Item| filter_product(item, $products, item.product_id))
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
product_id: ProductId,
|
||||
},
|
||||
Hover,
|
||||
Leave,
|
||||
}
|
||||
|
||||
impl From<CartMsg> for Msg {
|
||||
@ -28,15 +30,17 @@ pub type Items = indexmap::IndexMap<ProductId, Item>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Item {
|
||||
product_id: ProductId,
|
||||
quantity: Quantity,
|
||||
quantity_unit: QuantityUnit,
|
||||
pub product_id: ProductId,
|
||||
pub quantity: Quantity,
|
||||
pub quantity_unit: QuantityUnit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ShoppingCart {
|
||||
pub cart_id: Option<model::ShoppingCartId>,
|
||||
pub items: Items,
|
||||
#[serde(skip)]
|
||||
pub hover: bool,
|
||||
}
|
||||
|
||||
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 entry = items.entry(product_id).or_insert_with(|| Item {
|
||||
let entry: &mut Item = items.entry(product_id).or_insert_with(|| Item {
|
||||
quantity,
|
||||
quantity_unit,
|
||||
product_id,
|
||||
});
|
||||
entry.quantity = quantity;
|
||||
entry.quantity = entry.quantity + quantity;
|
||||
entry.quantity_unit = quantity_unit;
|
||||
}
|
||||
store_local(&model.cart);
|
||||
}
|
||||
CartMsg::ModifyItem { .. } => {}
|
||||
CartMsg::Hover => {
|
||||
model.cart.hover = true;
|
||||
}
|
||||
CartMsg::Leave => {
|
||||
model.cart.hover = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
LocalStorage::insert("ct", cart).ok();
|
||||
|
Loading…
Reference in New Issue
Block a user