Checkout page

This commit is contained in:
Adrian Woźniak 2022-05-18 15:40:50 +02:00
parent 66eb6395ac
commit 074c3a9900
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
15 changed files with 597 additions and 181 deletions

View File

@ -100,45 +100,38 @@ pub struct AddItem {
cart_async_handler!(AddItem, add_item, ShoppingCartItem);
async fn add_item(msg: AddItem, db: actix::Addr<Database>) -> Result<ShoppingCartItem> {
match db
.send(database_manager::EnsureActiveShoppingCart {
let _cart = query_db!(
db,
database_manager::EnsureActiveShoppingCart {
buyer_id: msg.buyer_id,
})
.await
{
Ok(Ok(_)) => {}
_ => return Err(Error::ShoppingCartFailed),
};
let cart = match db
.send(database_manager::AccountShoppingCarts {
},
Error::ShoppingCartFailed
);
let mut carts: Vec<model::ShoppingCart> = query_db!(
db,
database_manager::AccountShoppingCarts {
account_id: msg.buyer_id,
state: Some(ShoppingCartState::Active),
})
.await
.map(|res| match res {
Ok(mut v) if !v.is_empty() => Ok(v.remove(0)),
Err(e) => Err(Error::Db(e)),
_ => Err(Error::CartNotAvailable),
}) {
Ok(Ok(cart)) => cart,
Ok(Err(e)) => {
log::error!("{e:?}");
return Err(e);
}
_ => return Err(Error::CartNotAvailable),
},
passthrough Error::Db,
Error::CartNotAvailable
);
let cart = if carts.is_empty() {
return Err(Error::CartNotAvailable);
} else {
carts.remove(0)
};
match db
.send(database_manager::CreateShoppingCartItem {
Ok(query_db!(
db,
database_manager::CreateShoppingCartItem {
product_id: msg.product_id,
shopping_cart_id: cart.id,
quantity: msg.quantity,
quantity_unit: msg.quantity_unit,
})
.await
{
Ok(res) => res.map_err(Into::into),
_ => Err(Error::CantAddItem),
}
},
passthrough Error::Db,
Error::CantAddItem
))
}
#[derive(actix::Message)]

View File

@ -9,7 +9,7 @@ use model::api::Config;
#[macro_export]
macro_rules! public_send_db {
($db: expr, $msg: expr) => {{
use crate::routes::PublicError;
use $crate::routes::PublicError;
return match $db.send($msg).await {
Ok(Ok(res)) => Ok(HttpResponse::Ok().json(res)),
@ -59,8 +59,8 @@ async fn landing() -> HttpResponse {
HttpResponse::NotImplemented().body("")
}
#[get("/config.json")]
async fn client_config(config: Data<SharedAppConfig>) -> Json<model::api::Config> {
#[get("/config")]
async fn client_config(config: Data<SharedAppConfig>) -> Json<Config> {
let (optional_payment, currency) = {
let lock = config.lock();
let p = lock.payment();
@ -76,6 +76,8 @@ async fn client_config(config: Data<SharedAppConfig>) -> Json<model::api::Config
false => vec![model::PaymentMethod::PayU],
},
currency,
shipping: false,
shipping_methods: vec![],
})
}
@ -113,8 +115,8 @@ async fn svg(path: Path<String>) -> HttpResponse {
pub fn configure(config: &mut ServiceConfig) {
config
.service(client_config)
.service(landing)
.service(svg)
.service(client_config)
.configure(api_v1::configure);
}

View File

@ -17,7 +17,7 @@ pub struct Config {
pub coupons: bool,
pub currency: String,
pub shipping: bool,
pub shipping_methods: Vec<String>,
pub shipping_methods: Vec<ShippingMethod>,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]

View File

@ -591,7 +591,7 @@ where
{
fn decode(
value: <sqlx::Postgres as ::sqlx::database::HasValueRef<'r>>::ValueRef,
) -> Result<Self, Box<dyn Error + 'static + Send + Sync>> {
) -> Result<Self, Box<dyn ::std::error::Error + 'static + Send + Sync>> {
let value = <i32 as ::sqlx::decode::Decode<'r, sqlx::Postgres>>::decode(value)?;
Ok(Days(
(0..7)
@ -1099,10 +1099,12 @@ pub struct ProductPhoto {
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ShippingMethod {
/// Build-in InPost shipping
InPost,
Custom(String),
/// Shop owner will ship product manually
Manual,
}

View File

@ -12,3 +12,7 @@ backend = "http://localhost:8080/files"
[[proxy]]
rewrite = "/svg"
backend = "http://localhost:8080/svg"
[[proxy]]
rewrite = "/config"
backend = "http://localhost:8080/config"

View File

@ -4,7 +4,7 @@ use seed::fetch::{Header, Method, Request};
use crate::api::perform;
pub async fn config() -> super::NetRes<model::api::Config> {
perform(Request::new("/config.json").method(Method::Get)).await
perform(Request::new("/config").method(Method::Get)).await
}
pub async fn fetch_products() -> super::NetRes<model::api::Products> {

View File

@ -157,33 +157,38 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
shared::update(msg, model, orders);
}
Msg::UrlChanged(subs::UrlChanged(url)) => model.page.page_changed(url, orders),
Msg::Public(pages::public::Msg::Listing(msg)) => {
Msg::Public(pages::public::PublicMsg::Listing(msg)) => {
let page = fetch_page!(public model, Listing);
pages::public::listing::update(msg, page, &mut orders.proxy(Into::into));
}
Msg::Public(pages::public::Msg::Product(msg)) => {
Msg::Public(pages::public::PublicMsg::Product(msg)) => {
let page = fetch_page!(public model, Product);
pages::public::product::update(msg, page, &mut orders.proxy(Into::into))
}
Msg::Public(pages::public::Msg::SignIn(msg)) => {
Msg::Public(pages::public::PublicMsg::SignIn(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(
Msg::Public(pages::public::PublicMsg::SignUp(
pages::public::sign_up::RegisterMsg::AccountCreated(res),
)) => {
orders
.skip()
.send_msg(Msg::Session(SessionMsg::TokenRefreshed(res)));
}
Msg::Public(pages::public::Msg::SignUp(msg)) => {
Msg::Public(pages::public::PublicMsg::SignUp(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)) => {
Msg::Public(pages::public::PublicMsg::ShoppingCart(msg)) => {
let page = fetch_page!(public model, ShoppingCart);
pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into))
}
Msg::Public(pages::public::PublicMsg::Checkout(msg)) => {
let page = fetch_page!(public model, Checkout);
pages::public::checkout::update(msg, page, &mut orders.proxy(Into::into))
}
// Admin
Msg::Admin(pages::admin::Msg::Landing(msg)) => {
let page = fetch_page!(admin model, Landing);
pages::admin::landing::update(msg, page, &mut orders.proxy(Into::into))
@ -210,6 +215,7 @@ fn view(model: &Model) -> Node<Msg> {
Page::Public(PublicPage::ShoppingCart(page)) => {
pages::public::shopping_cart::view(model, page)
}
Page::Public(PublicPage::Checkout(page)) => pages::public::checkout::view(model, page),
Page::Admin(AdminPage::Landing(page)) => pages::admin::landing::view(model, page),
_ => empty![],
};

View File

@ -24,6 +24,8 @@ pub struct Config {
pub pay_methods: Vec<::model::PaymentMethod>,
pub coupons: bool,
pub currency: &'static rusty_money::iso::Currency,
pub shipping: bool,
pub shipping_methods: Vec<model::ShippingMethod>,
}
impl Default for Config {
@ -32,6 +34,8 @@ impl Default for Config {
pay_methods: vec![],
coupons: false,
currency: rusty_money::iso::PLN,
shipping: false,
shipping_methods: vec![],
}
}
}
@ -42,12 +46,16 @@ impl From<::model::api::Config> for Config {
pay_methods,
coupons,
currency,
shipping,
shipping_methods,
}: model::api::Config,
) -> Self {
Self {
pay_methods,
coupons,
currency: rusty_money::iso::find(&currency).unwrap_or(rusty_money::iso::PLN),
shipping,
shipping_methods,
}
}
}

View File

@ -6,12 +6,40 @@ use seed::{struct_urls, Url};
use crate::{shared, NetRes};
/// Implement msg conversion for
///
/// Examples
///
/// ```
/// use crate::pages::*;
/// use crate::pages::public::*;
/// use crate::pages::public::checkout::*;
///
/// impl_into_msg!(ShoppingCartMsg > PublicMsg as ShoppingCart > Public);
/// ```
#[macro_export]
macro_rules! impl_into_msg {
($msg: ty > $scoped_msg: ty as $scope_name: ident > $scope: ident) => {
impl From<$msg> for $crate::Msg {
fn from(msg: $msg) -> $crate::Msg {
$crate::Msg::$scope(msg.into())
}
}
impl From<$msg> for $scoped_msg {
fn from(msg: $msg) -> $scoped_msg {
<$scoped_msg>::$scope_name(msg)
}
}
};
}
#[derive(Debug)]
pub enum Msg {
#[cfg(debug_assertions)]
NoOp,
Config(NetRes<::model::api::Config>),
Public(public::Msg),
Public(public::PublicMsg),
Admin(admin::Msg),
UrlChanged(subs::UrlChanged),
Shared(shared::SharedMsg),
@ -36,7 +64,7 @@ pub enum PublicPage {
SignIn(public::sign_in::SignInPage),
SignUp(public::sign_up::SignUpPage),
ShoppingCart(public::shopping_cart::ShoppingCartPage),
Checkout,
Checkout(public::checkout::CheckoutPage),
}
#[derive(Debug)]
@ -63,6 +91,10 @@ impl Page {
["shopping-cart", _rest @ ..] => Self::Public(PublicPage::ShoppingCart(
public::shopping_cart::init(url, &mut orders.proxy(Into::into)),
)),
["checkout", _rest @ ..] => Self::Public(PublicPage::Checkout(public::checkout::init(
url,
&mut orders.proxy(Into::into),
))),
["sign-in", _rest @ ..] => Self::Public(PublicPage::SignIn(public::sign_in::init(
url,
&mut orders.proxy(Into::into),
@ -101,6 +133,10 @@ impl Page {
crate::fetch_page!(public page self, ShoppingCart, Page::init(url, orders));
public::shopping_cart::page_changed(url, page);
}
["checkout", _rest @ ..] => {
let page = crate::fetch_page!(public page self, Checkout, Page::init(url, orders));
public::checkout::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);

View File

@ -1,3 +1,5 @@
use crate::impl_into_msg;
pub mod checkout;
pub mod listing;
pub mod product;
@ -6,79 +8,21 @@ pub mod sign_in;
pub mod sign_up;
#[derive(Debug)]
pub enum Msg {
pub enum PublicMsg {
Listing(listing::ListingMsg),
Product(product::ProductMsg),
SignIn(sign_in::LogInMsg),
SignUp(sign_up::RegisterMsg),
ShoppingCart(shopping_cart::ShoppingCartMsg),
Checkout(checkout::CheckoutMsg),
}
impl From<listing::ListingMsg> for Msg {
fn from(msg: listing::ListingMsg) -> Self {
Self::Listing(msg)
}
}
impl From<product::ProductMsg> for Msg {
fn from(msg: product::ProductMsg) -> Self {
Self::Product(msg)
}
}
impl From<sign_in::LogInMsg> for Msg {
fn from(msg: sign_in::LogInMsg) -> Self {
Self::SignIn(msg)
}
}
impl From<sign_up::RegisterMsg> for Msg {
fn from(msg: sign_up::RegisterMsg) -> Self {
Self::SignUp(msg)
}
}
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::ProductMsg> for crate::Msg {
fn from(msg: product::ProductMsg) -> Self {
crate::Msg::Public(msg.into())
}
}
impl From<sign_in::LogInMsg> for crate::Msg {
fn from(msg: sign_in::LogInMsg) -> Self {
crate::Msg::Public(msg.into())
}
}
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)
}
}
impl_into_msg!(listing::ListingMsg > PublicMsg as Listing > Public);
impl_into_msg!(product::ProductMsg > PublicMsg as Product > Public);
impl_into_msg!(sign_in::LogInMsg > PublicMsg as SignIn > Public);
impl_into_msg!(sign_up::RegisterMsg > PublicMsg as SignUp > Public);
impl_into_msg!(shopping_cart::ShoppingCartMsg > PublicMsg as ShoppingCart > Public);
impl_into_msg!(checkout::CheckoutMsg > PublicMsg as Checkout > Public);
pub mod layout {
use seed::prelude::*;

View File

@ -0,0 +1,414 @@
use seed::prelude::*;
use seed::*;
use crate::model::Products;
use crate::NetRes;
#[derive(Debug)]
pub enum CheckoutMsg {
ProductsFetched(NetRes<model::api::Products>),
}
#[derive(Debug)]
pub struct CheckoutPage {
pub products: Products,
}
pub fn init(_url: Url, orders: &mut impl Orders<crate::Msg>) -> CheckoutPage {
orders.perform_cmd(async move {
crate::Msg::from(CheckoutMsg::ProductsFetched(
crate::api::public::fetch_products().await,
))
});
CheckoutPage {
products: Default::default(),
}
}
pub fn page_changed(_url: Url, _model: &mut CheckoutPage) {}
pub fn update(msg: CheckoutMsg, model: &mut CheckoutPage, _orders: &mut impl Orders<crate::Msg>) {
match msg {
CheckoutMsg::ProductsFetched(NetRes::Success(products)) => {
model.products.update(products.0);
}
CheckoutMsg::ProductsFetched(NetRes::Error(e)) => {
seed::error!("fetch product error", e);
}
CheckoutMsg::ProductsFetched(NetRes::Http(e)) => {
seed::error!("fetch product http", e);
}
}
}
pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node<crate::Msg> {
let content = div![
C!["flex justify-center my-6"],
div![
C!["flex flex-col w-full p-8 text-gray-800 bg-white shadow-lg pin-r pin-y md:w-4/5 lg:w-4/5"],
div![C!["-mx-3 md:flex items-start"], left_side::view(model, page), right_side::view(model, page)]
]
];
div![
crate::shared::view::public_navbar::view(model, &page.products),
super::layout::view(model, content, None)
]
}
mod left_side {
use rusty_money::Money;
use seed::prelude::*;
use seed::*;
use crate::pages::public::checkout::CheckoutPage;
use crate::shopping_cart::Item;
use crate::Msg;
pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node<Msg> {
div![
C!["px-3 md:w-7/12 lg:pr-10"],
products(model, page),
total(model, page)
]
}
fn products(model: &crate::Model, page: &CheckoutPage) -> Node<Msg> {
let products = model
.cart
.items
.values()
.filter_map(|item: &Item| {
page.products
.products
.get(&item.product_id)
.map(|product| (item, product))
})
.map(|(item, product)| product_view(model, product, item));
div![
C!["w-full mx-auto text-gray-800 font-light mb-6 border-b border-gray-200 pb-6"],
products
]
}
fn product_view(model: &crate::Model, product: &model::api::Product, item: &Item) -> Node<Msg> {
let img = product
.photos
.first()
.as_ref()
.map(|photo| photo.url.as_str())
.unwrap_or_default();
let price = Money::from_minor(
**(product.price * item.quantity) as i64,
model.config.currency,
)
.to_string();
let split = if price.contains(',') { ',' } else { '.' };
let mut price_parts = price.split(split);
div![
C!["w-full flex items-center"],
div![
C!["overflow-hidden rounded-lg w-16 h-16 bg-gray-50 border border-gray-200"],
img![attrs![At::Src => img]]
],
div![
C!["flex-grow pl-3"],
h6![
C!["font-semibold uppercase text-gray-600"],
product.name.as_str()
],
p![C!["text-gray-400"], "x ", **item.quantity]
],
div![
span![
C!["font-semibold text-gray-600 text-xl"],
price_parts.next()
],
span![
C!["font-semibold text-gray-600 text-sm"],
split.to_string(),
price_parts.next().unwrap_or("00")
],
]
]
}
fn total(model: &crate::Model, page: &CheckoutPage) -> Node<Msg> {
let subtotal_value = 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)| {
**(product.price * item.quantity)
},
)
.sum::<i32>() as i64;
// TODO: Shipping
let shipping_cost = 0;
let total = subtotal_value + shipping_cost;
div![
C!["mb-6 pb-6 border-b border-gray-200 md:border-none text-gray-800 text-xl"],
div![
C!["w-full flex items-center"],
div![
C!["flex-grow"],
span![C!["text-gray-600"], model.i18n.t("Total")]
],
div![
C!["pl-3"],
span![
C!["font-semibold"],
Money::from_minor(total, model.config.currency).to_string()
]
]
]
]
}
}
mod right_side {
use model::PaymentMethod;
use seed::prelude::*;
use seed::*;
use crate::pages::public::checkout::CheckoutPage;
use crate::Msg;
static SELECTED_PAYMENT_METHOD: &str = "payment-selected-method";
pub fn view(model: &crate::Model, page: &CheckoutPage) -> Node<Msg> {
let pay_methods = model.config.pay_methods.iter().map(|ty| match ty {
PaymentMethod::PayU => pay_u(model),
PaymentMethod::PaymentOnTheSpot => pay_on_spot(model),
});
div![
C!["px-3 md:w-5/12"],
contact(model, page),
div![
C!["w-full mx-auto rounded-lg bg-white border border-gray-200 text-gray-800 font-light mb-6"],
pay_methods
],
pay_now(model)
]
}
fn contact(model: &crate::Model, _page: &CheckoutPage) -> Node<Msg> {
if model.shared.me.is_some() {
// TODO: Display user addresses
return empty![];
}
div![
C!["w-full mx-auto rounded-lg bg-white border border-gray-200 p-3 text-gray-800 font-light mb-6"],
form![
div![
C!["mb-3"],
div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Contact")],
],
div![
C!["mb-3"],
div![
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-name", At::Placeholder => model.i18n.t("Name")]
]
],
],
div![
C!["mb-3"],
div![
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-email", At::Type => "email", At::Placeholder => model.i18n.t("E-Mail")]
]
],
],
div![
C!["mb-3"],
div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Address")],
],
div![
C!["mb-3"],
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-street", At::Placeholder => model.i18n.t("Street")]
]
],
div![
C!["mb-3"],
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-city", At::Placeholder => model.i18n.t("City")]
]
],
div![
C!["mb-3 inline-block w-1/2 pr-1"],
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-country", At::Placeholder => model.i18n.t("Country")]
]
],
div![
C!["mb-3 inline-block -mx-1 pl-1 w-1/2"],
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-zip", At::Placeholder => model.i18n.t("Zip")]
]
],
]
]
}
fn pay_now(model: &crate::Model) -> Node<Msg> {
div![
button![
C!["w-full max-w-xs mx-auto bg-indigo-500 hover:bg-indigo-700 focus:bg-indigo-700 text-white rounded-lg px-3 py-2 font-semibold text-center flex justify-center"],
padlock_icon(),
model.i18n.t("PAY NOW"),
]
]
}
fn padlock_icon() -> Node<Msg> {
svg![
attrs![At::Xmlns => "http://www.w3.org/2000/svg", At::ViewBox => "0 0 32 32"],
C!["fill-white h-6 mr-3"],
path![
attrs![At::D => "M25 13H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V15a2 2 0 0 0-2-2zM7 28V15h18v13zm2-17a1 1 0 0 0 1-1V8a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2a1 1 0 0 0 2 0V8a6 6 0 0 0-6-6h-4a6 6 0 0 0-6 6v2a1 1 0 0 0 1 1z"]
],
path![attrs![At::D => "M16 18a1 1 0 0 0-1 1v6a1 1 0 0 0 2 0v-6a1 1 0 0 0-1-1Z"]],
]
}
fn pay_u(model: &crate::Model) -> Node<Msg> {
div![
C!["w-full p-3 border-b border-gray-200"],
label![
C!["flex items-center cursor-pointer"],
attrs![At::For => "pay_u"],
input![
C!["form-radio h-5 w-5 text-indigo-500"],
attrs![At::Id => "pay_u", At::Name => SELECTED_PAYMENT_METHOD, At::Type => "radio", At::Value => "pay_u"],
],
span![
C!["flex items-center"],
pay_u_icon(),
span![C!["ml-3"], model.i18n.t("PayU")]
]
],
]
}
fn pay_u_icon() -> Node<Msg> {
svg![
C!["ml-3"],
attrs![
At::Xmlns => "http://www.w3.org/2000/svg",
At::ViewBox => "0 0 120 76",
At::Style => "height: 21px; width: 33px;",
],
g![
attrs![
At::Fill => "none",
At::FillRule => "evenodd"
],
path![attrs![
At::Fill => "#1AAF5D",
At::D => "M111.999 0H8C3.582 0 0 3.59 0 8.008v59.984C0 72.415 3.591 76 8.001 76H112c4.419 0 8.001-3.59 8.001-8.008V8.008C120 3.585 116.409 0 111.999 0Z"
]],
path![attrs![
At::D => "M89.794 27.902H88.57a2.243 2.243 0 0 0-2.242 2.243V44.22s-.116 2.447-4.181 2.447c-4.066 0-4.181-2.447-4.181-2.447V30.145a2.243 2.243 0 0 0-2.242-2.243h-2.451a2.243 2.243 0 0 0-2.242 2.242V43.61c0 4.841 3.927 8.768 8.77 8.768h4.691a8.77 8.77 0 0 0 8.77-8.768v-10.2h2.653a1.02 1.02 0 0 0 1.019-1.019v-5.1a1.02 1.02 0 0 0-1.019-1.02h-5.1a1.02 1.02 0 0 0-1.02 1.02v.612zm0 0h1.226a2.244 2.244 0 0 1 2.242 2.242v3.265h-2.449a1.02 1.02 0 0 1-1.019-1.019v-4.488zm6.935-6.118c0-.45.365-.817.817-.817h3.67c.45 0 .816.365.816.817v3.67a.816.816 0 0 1-.817.816h-3.669a.816.816 0 0 1-.817-.817v-3.669zm-3.467-3.674c0-.337.276-.61.61-.61h2.45c.338 0 .611.276.611.61v2.45c0 .338-.276.611-.61.611h-2.45a.612.612 0 0 1-.611-.61v-2.45zM61.089 52.382a36.48 36.48 0 0 1-.463.073c-3.95.513-4.733-2.421-5.419-4.66L51.767 35c-.175-.654.229-1.183.907-1.183h.815c.676 0 1.366.528 1.542 1.182l3.443 12.809c.373.945.85 1.82 1.883 1.82 1.035 0 1.577-.214 2.05-1.977l2.986-12.643c.156-.658.83-1.191 1.507-1.191h.802c.677 0 1.095.534.935 1.19l-3.948 16.17c-1.817 7.135-4.153 8.173-7.927 8.323-1.152-.15-1.745-2.488-.805-2.866 0 0 1.902.013 3.002-.784 1.188-.862 1.693-2.24 1.968-3.026.016-.003.143-.397.162-.442zM47.37 41.363h-6.218a5.406 5.406 0 1 0 0 10.81h4.075a5.406 5.406 0 0 0 5.407-5.405v-7.852c0-5.507-5.92-5.507-7.547-5.507-2.104 0-4.697.318-4.697.318a.93.93 0 0 0-.81.904v1.227c0 .45.363.776.81.729 0 0 2.844-.322 4.539-.322 1.62 0 4.441 0 4.441 2.65v2.448zm-8.362 5.303a2.85 2.85 0 0 1 2.854-2.855h5.508v2.855a2.85 2.85 0 0 1-2.854 2.856h-2.654a2.857 2.857 0 0 1-2.854-2.856zM18 31.37a3.467 3.467 0 0 1 3.469-3.468h7.545a6.73 6.73 0 0 1 6.73 6.729v2.247a6.73 6.73 0 0 1-6.73 6.73h-7.547v7.545a1.02 1.02 0 0 1-1.021 1.02h-1.424A1.021 1.021 0 0 1 18 51.153V31.37zm3.467.795c0-.552.446-1 1.002-1h5.421a4.59 4.59 0 1 1 0 9.179h-6.423v-8.179z",
At::Fill => "#FFF"
]]
]
]
}
fn pay_on_spot(model: &crate::Model) -> Node<Msg> {
div![
C!["w-full p-3 border-b border-gray-200"],
label![
C!["flex items-center cursor-pointer"],
attrs![At::For => "pay_on_spot"],
input![
C!["form-radio h-5 w-5 text-indigo-500"],
attrs![At::Id => "pay_on_spot", At::Name => SELECTED_PAYMENT_METHOD, At::Type => "radio", At::Value => "pay_on_spot"],
],
span![
C!["flex items-center"],
pay_in_spot_icon(),
span![C!["ml-3"], model.i18n.t("Pay on spot")]
]
],
]
}
fn pay_in_spot_icon() -> Node<Msg> {
svg![
attrs![
At::Style => "height: 21px; width: 33px;",
At::ViewBox => "0 0 31 17",
At::Xmlns => "http://www.w3.org/2000/svg"
],
C!["ml-3"],
g![
attrs!["transform" => "translate(120.5 -1344.862)"],
rect![attrs![
At::Width => "31" ,
At::Height => "17",
At::X => "-120.5",
At::Y => "1344.862",
At::Fill => "#2dbca4",
At::Rx => "1.5",
At::Ry => "1.5"
]],
path![attrs![
At::Fill => "#15a78f",
At::D => "M-115.5 1346.862a3 3 0 0 1-3 3v7a3 3 0 0 1 3 3h21a3 3 0 0 1 3-3v-7a3 3 0 0 1-3-3z"
]],
circle![attrs![
At::Cx => "-115",
At::Cy => "1353.362",
At::R => ".5",
At::Fill => "#2dbca4",
At::Stroke => "#0c5286",
At::StrokeLinecap => "round",
"stroke-miterlimit" => "3.7"
]],
circle![attrs![
At::Cx => "-95",
At::Cy => "1353.362",
At::R => ".5",
At::Fill => "#2dbca4",
At::Stroke => "#0c5286",
At::StrokeLinecap => "round",
"stroke-miterlimit" => "3.7"
]],
path![attrs![
At::Fill => "#2dbca4",
At::D => "M-99.5 1353.362c0 3.036-2.474 5.5-5.516 5.5a5.484 5.484 0 0 1-5.484-5.5c0-3.035 2.44-5.5 5.484-5.5 3.042 0 5.516 2.465 5.516 5.5z"
]],
path![attrs![
At::Fill => "#0c5286",
At::Style => "line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;isolation:auto;mix-blend-mode:normal",
At::D => "M-105.49 327.998v1.191a2.476 2.476 0 0 0-1.065.442c-.701.495-1.072 1.398-.812 2.256.297 1.033 1.312 1.631 2.31 1.613h.121a.5.5 0 0 0 .012 0c.571-.011 1.188.372 1.336.893a.5.5 0 0 0 .002.011c.122.398-.075.908-.432 1.157a.5.5 0 0 0-.015.01c-.6.448-1.647.327-2.082-.28a1.051 1.051 0 0 1-.201-.607h-1c-.001.433.142.85.39 1.195a2.36 2.36 0 0 0 1.436.92V338h1v-1.183a2.476 2.476 0 0 0 1.047-.436c.703-.494 1.074-1.397.814-2.256-.297-1.034-1.316-1.631-2.314-1.613h-.12a.5.5 0 0 0-.011 0c-.571.011-1.186-.372-1.334-.893a.5.5 0 0 0-.002-.011c-.122-.398.075-.908.432-1.157a.5.5 0 0 0 .013-.011c.6-.449 1.646-.326 2.08.281a.5.5 0 0 0 .002 0c.129.177.202.397.201.607h1a2.03 2.03 0 0 0-.388-1.195 2.36 2.36 0 0 0-1.42-.916v-1.219z",
At::Color => "#000",
At::FontFamily => "sans-serif",
At::FontWeight => "400",
At::Overflow => "visible",
At::Transform => "translate(0 1020.362)",
]],
]
]
}
}

View File

@ -70,69 +70,76 @@ pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
fn summary(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
C!["my-4 mt-6 -mx-2 lg:flex"],
summary_left(model, page),
summary_left::view(model, page),
summary_right::view(model, page)
]
}
fn summary_left(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
C!["lg:px-2 lg:w-1/2"],
IF![model.config.coupons => div![C!["p-4 bg-gray-100 rounded-full"], h1![C!["ml-2 font-bold uppercase"], model.i18n.t("Coupon Code")]]],
IF![model.config.coupons => coupon_form(model, page)],
div![
C!["p-4 bg-gray-100 rounded-full", IF![model.config.coupons => "mt-6"]],
div![
C!["ml-2 font-bold uppercase"],
model.i18n.t("Instruction for seller")
]
],
div![C!["p-4"], p![C!["mb-4 italic"], model.i18n.t("If you have some information for the seller you can leave them in the box below")]],
textarea![C!["w-full h-24 p-2 bg-gray-100 rounded border-none"]]
]
}
mod summary_left {
use seed::prelude::*;
use seed::*;
fn coupon_form(model: &crate::Model, _page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
C!["p-4"],
p![
C!["mb-4 italic"],
model
.i18n
.t("If you have a coupon code, please enter it in the box below")
],
use crate::pages::public::shopping_cart::ShoppingCartPage;
pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
C!["justify-center md:flex"],
form![
C!["lg:px-2 lg:w-1/2"],
IF![model.config.coupons => div![C!["p-4 bg-gray-100 rounded-full"], h1![C!["ml-2 font-bold uppercase"], model.i18n.t("Coupon Code")]]],
IF![model.config.coupons => coupon_form(model, page)],
div![
C!["p-4 bg-gray-100 rounded-full", IF![model.config.coupons => "mt-6"]],
div![
C!["flex items-center w-full h-13 pl-3 bg-white bg-gray-100 border rounded-full"],
input![C!["w-full bg-gray-100 outline-none appearance-none focus:outline-none active:outline-none"]],
button![
C!["text-sm flex items-center px-3 py-1 text-white bg-gray-800 rounded-full outline-none md:px-4 hover:bg-gray-700 focus:outline-none active:outline-none"],
gift_icon(),
span![C!["font-medium"], model.i18n.t("Apply coupon")]
C!["ml-2 font-bold uppercase"],
model.i18n.t("Instruction for seller")
]
],
div![C!["p-4"], p![C!["mb-4 italic"], model.i18n.t("If you have some information for the seller you can leave them in the box below")]],
textarea![C!["w-full h-24 p-2 bg-gray-100 rounded border-none"]]
]
}
fn coupon_form(model: &crate::Model, _page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
C!["p-4"],
p![
C!["mb-4 italic"],
model
.i18n
.t("If you have a coupon code, please enter it in the box below")
],
div![
C!["justify-center md:flex"],
form![
div![
C!["flex items-center w-full h-13 pl-3 bg-white bg-gray-100 border rounded-full"],
input![C!["w-full bg-gray-100 outline-none appearance-none focus:outline-none active:outline-none"]],
button![
C!["text-sm flex items-center px-3 py-1 text-white bg-gray-800 rounded-full outline-none md:px-4 hover:bg-gray-700 focus:outline-none active:outline-none"],
gift_icon(),
span![C!["font-medium"], model.i18n.t("Apply coupon")]
]
]
]
]
]
]
}
}
fn gift_icon() -> Node<crate::Msg> {
svg![
attrs![
"aria-hidden" => "true",
"data-prefix" => "fas",
"data-icon" => "gift",
"class" => "w-8",
"xmlns" => "http://www.w3.org/2000/svg",
"viewBox" => "0 0 512 512"
],
path![attrs![
"fill" => "currentColor",
"d" => "M32 448c0 17.7 14.3 32 32 32h160V320H32v128zm256 32h160c17.7 0 32-14.3 32-32V320H288v160zm192-320h-42.1c6.2-12.1 10.1-25.5 10.1-40 0-48.5-39.5-88-88-88-41.6 0-68.5 21.3-103 68.3-34.5-47-61.4-68.3-103-68.3-48.5 0-88 39.5-88 88 0 14.5 3.8 27.9 10.1 40H32c-17.7 0-32 14.3-32 32v80c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16v-80c0-17.7-14.3-32-32-32zm-326.1 0c-22.1 0-40-17.9-40-40s17.9-40 40-40c19.9 0 34.6 3.3 86.1 80h-86.1zm206.1 0h-86.1c51.4-76.5 65.7-80 86.1-80 22.1 0 40 17.9 40 40s-17.9 40-40 40z"
]]
]
fn gift_icon() -> Node<crate::Msg> {
svg![
attrs![
"aria-hidden" => "true",
"data-prefix" => "fas",
"data-icon" => "gift",
"class" => "w-8",
At::Xmlns => "http://www.w3.org/2000/svg",
"viewBox" => "0 0 512 512"
],
path![attrs![
"fill" => "currentColor",
"d" => "M32 448c0 17.7 14.3 32 32 32h160V320H32v128zm256 32h160c17.7 0 32-14.3 32-32V320H288v160zm192-320h-42.1c6.2-12.1 10.1-25.5 10.1-40 0-48.5-39.5-88-88-88-41.6 0-68.5 21.3-103 68.3-34.5-47-61.4-68.3-103-68.3-48.5 0-88 39.5-88 88 0 14.5 3.8 27.9 10.1 40H32c-17.7 0-32 14.3-32 32v80c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16v-80c0-17.7-14.3-32-32-32zm-326.1 0c-22.1 0-40-17.9-40-40s17.9-40 40-40c19.9 0 34.6 3.3 86.1 80h-86.1zm206.1 0h-86.1c51.4-76.5 65.7-80 86.1-80 22.1 0 40 17.9 40 40s-17.9 40-40 40z"
]]
]
}
}
mod summary_right {
@ -307,13 +314,13 @@ mod summary_right {
"aria-hidden" => "true",
"data-prefix" => "far",
"data-icon" => "trash-alt",
"class" => "w-4 text-red-600 hover:text-red-800",
"xmlns" => "http://www.w3.org/2000/svg",
"viewBox" => "0 0 448 512"
At::Xmlns => "http://www.w3.org/2000/svg",
At::ViewBox => "0 0 448 512"
],
C!["w-4 text-red-600 hover:text-red-800"],
path![attrs![
"fill" => "currentColor",
"d" => "M268 416h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12zM432 80h-82.41l-34-56.7A48 48 0 00274.41 0H173.59a48 48 0 00-41.16 23.3L98.41 80H16A16 16 0 000 96v16a16 16 0 0016 16h16v336a48 48 0 0048 48h288a48 48 0 0048-48V128h16a16 16 0 0016-16V96a16 16 0 00-16-16zM171.84 50.91A6 6 0 01177 48h94a6 6 0 015.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12z"
At::Fill => "currentColor",
At::D => "M268 416h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12zM432 80h-82.41l-34-56.7A48 48 0 00274.41 0H173.59a48 48 0 00-41.16 23.3L98.41 80H16A16 16 0 000 96v16a16 16 0 0016 16h16v336a48 48 0 0048 48h288a48 48 0 0048-48V128h16a16 16 0 0016-16V96a16 16 0 00-16-16zM171.84 50.91A6 6 0 01177 48h94a6 6 0 015.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0012-12V188a12 12 0 00-12-12h-24a12 12 0 00-12 12v216a12 12 0 0012 12z"
]]
]
}
@ -324,13 +331,13 @@ mod summary_right {
"aria-hidden" => "true",
"data-prefix" => "far",
"data-icon" => "credit-card",
"class" => "w-8",
"xmlns" => "http://www.w3.org/2000/svg",
"viewBox" => "0 0 576 512"
At::Xmlns => "http://www.w3.org/2000/svg",
At::ViewBox => "0 0 576 512"
],
C!["w-8"],
path![attrs![
"fill" => "currentColor",
"d" => "M527.9 32H48.1C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48.1 48h479.8c26.6 0 48.1-21.5 48.1-48V80c0-26.5-21.5-48-48.1-48zM54.1 80h467.8c3.3 0 6 2.7 6 6v42H48.1V86c0-3.3 2.7-6 6-6zm467.8 352H54.1c-3.3 0-6-2.7-6-6V256h479.8v170c0 3.3-2.7 6-6 6zM192 332v40c0 6.6-5.4 12-12 12h-72c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h72c6.6 0 12 5.4 12 12zm192 0v40c0 6.6-5.4 12-12 12H236c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h136c6.6 0 12 5.4 12 12z"
At::Fill => "currentColor",
At::D => "M527.9 32H48.1C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48.1 48h479.8c26.6 0 48.1-21.5 48.1-48V80c0-26.5-21.5-48-48.1-48zM54.1 80h467.8c3.3 0 6 2.7 6 6v42H48.1V86c0-3.3 2.7-6 6-6zm467.8 352H54.1c-3.3 0-6-2.7-6-6V256h479.8v170c0 3.3-2.7 6-6 6zM192 332v40c0 6.6-5.4 12-12 12h-72c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h72c6.6 0 12 5.4 12 12zm192 0v40c0 6.6-5.4 12-12 12H236c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h136c6.6 0 12 5.4 12 12z"
]]
]
}

View File

@ -61,7 +61,7 @@ pub fn redirect_on_session(model: &Model, orders: &mut impl Orders<Msg>) {
PublicPage::SignIn(_) => {}
PublicPage::SignUp(_) => {}
PublicPage::ShoppingCart(_) => {}
PublicPage::Checkout => {}
PublicPage::Checkout(_) => {}
},
}
}

View File

@ -162,7 +162,7 @@ pub fn message(id: uuid::Uuid, message: &str, icon: Type) -> Node<Msg> {
fn icon(color: &str, d: &str) -> Node<Msg> {
svg![
attrs![
"xmlns"=>"http://www.w3.org/2000/svg",
At::Xmlns=>"http://www.w3.org/2000/svg",
"class" => "w-8 h-8",
"viewBox" => "0 0 20 20",
"fill" => "currentColor"
@ -218,7 +218,7 @@ fn close_icon() -> Node<Msg> {
C!["inline-flex items-center cursor-pointer"],
svg![
attrs![
"xmlns" => "http://www.w3.org/2000/svg",
At::Xmlns => "http://www.w3.org/2000/svg",
"class" => "w-4 h-4 text-gray-600",
"fill" => "none",
"viewBox" => "0 0 24 24",

View File

@ -76,7 +76,7 @@ pub mod public_navbar {
"width" => "32px",
"height" => "32px",
At::ViewBox => "0 0 32 32",
"xmlns" => "http://www.w3.org/2000/svg",
At::Xmlns => "http://www.w3.org/2000/svg",
At::Class => "w-6 h-6",
At::Fill => "none",
At::Stroke => "currentColor",
@ -91,7 +91,7 @@ pub mod public_navbar {
fn account() -> Node<Msg> {
svg![
attrs![
"xmlns" => "http://www.w3.org/2000/svg",
At::Xmlns => "http://www.w3.org/2000/svg",
At::Class => "w-6 h-6",
At::Fill => "none",
At::ViewBox => "0 0 24 24",
@ -180,7 +180,7 @@ pub mod cart_dropdown {
fn trash_icon() -> Node<Msg> {
svg![
attrs![
"xmlns" => Namespace::Svg.as_str(),
At::Xmlns => Namespace::Svg.as_str(),
"width" => "100%",
"height" => "100%",
"fill" => "none",