Shopping cart view, drop down shopping cart

This commit is contained in:
Adrian Woźniak 2022-05-16 16:08:14 +02:00
parent ad924abd3f
commit 4db6cf7327
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
17 changed files with 690 additions and 296 deletions

75
Cargo.lock generated
View File

@ -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",

View File

@ -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;

View File

@ -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"] }

View File

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

View File

@ -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![],
}; };

View File

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

View File

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

View File

@ -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)

View File

@ -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))
] ]
} }

View File

@ -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()]]

View 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()
]
]
]
}

View File

@ -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![

View File

@ -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![

View File

@ -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 => {}
}, },
} }

View File

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

View File

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

View File

@ -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();