From c1c97061eb3c5e8d8b8cf2ccd123773a9c592af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Mon, 23 May 2022 14:11:56 +0200 Subject: [PATCH] WIP: Place order for guess and account. --- actors/account_manager/src/lib.rs | 2 +- .../{addresses.rs => account_addresses.rs} | 76 ++++++++-- actors/database_manager/src/lib.rs | 16 ++- .../database_manager/src/order_addresses.rs | 130 ++++++++++++++++++ actors/database_manager/src/order_items.rs | 4 +- .../src/{account_orders.rs => orders.rs} | 83 +++++------ actors/order_manager/src/lib.rs | 99 +++++++++++-- actors/payment_manager/src/lib.rs | 4 +- api/src/routes/admin/api_v1/accounts.rs | 4 +- api/src/routes/admin/api_v1/orders.rs | 6 +- api/src/routes/public/api_v1/restricted.rs | 29 ++++ api/src/routes/public/mod.rs | 2 + migrations/20220523090806_change_orders.sql | 19 +++ ...3095745_add_default_to_account_address.sql | 2 + shared/model/src/api.rs | 68 ++++++--- shared/model/src/lib.rs | 61 +++++--- web/src/api/public.rs | 21 +++ web/src/pages/public/checkout.rs | 120 +++++++++++----- web/src/pages/public/sign_up.rs | 56 ++++---- 19 files changed, 609 insertions(+), 193 deletions(-) rename actors/database_manager/src/{addresses.rs => account_addresses.rs} (62%) create mode 100644 actors/database_manager/src/order_addresses.rs rename actors/database_manager/src/{account_orders.rs => orders.rs} (75%) create mode 100644 migrations/20220523090806_change_orders.sql create mode 100644 migrations/20220523095745_add_default_to_account_address.sql diff --git a/actors/account_manager/src/lib.rs b/actors/account_manager/src/lib.rs index e1611ae..847954e 100644 --- a/actors/account_manager/src/lib.rs +++ b/actors/account_manager/src/lib.rs @@ -90,7 +90,7 @@ impl actix::Actor for AccountManager { pub struct MeResult { pub account: FullAccount, - pub addresses: Vec, + pub addresses: Vec, } #[derive(actix::Message, Debug)] diff --git a/actors/database_manager/src/addresses.rs b/actors/database_manager/src/account_addresses.rs similarity index 62% rename from actors/database_manager/src/addresses.rs rename to actors/database_manager/src/account_addresses.rs index 928a1e9..c143e69 100644 --- a/actors/database_manager/src/addresses.rs +++ b/actors/database_manager/src/account_addresses.rs @@ -9,7 +9,7 @@ pub enum Error { } #[derive(actix::Message)] -#[rtype(result = "Result>")] +#[rtype(result = "Result>")] pub struct AccountAddresses { pub account_id: model::AccountId, } @@ -17,17 +17,17 @@ pub struct AccountAddresses { db_async_handler!( AccountAddresses, account_addresses, - Vec, + Vec, inner_account_addresses ); pub(crate) async fn account_addresses( msg: AccountAddresses, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, -) -> Result> { +) -> Result> { sqlx::query_as( r#" -SELECT id, name, email, street, city, country, zip, account_id +SELECT id, name, email, street, city, country, zip, account_id, is_default FROM account_addresses WHERE account_id = $1 "#, @@ -39,7 +39,37 @@ WHERE account_id = $1 } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] +pub struct DefaultAccountAddress { + pub account_id: model::AccountId, +} + +db_async_handler!( + DefaultAccountAddress, + default_account_address, + model::AccountAddress, + inner_default_account_address +); + +pub(crate) async fn default_account_address( + msg: DefaultAccountAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +SELECT id, name, email, street, city, country, zip, account_id, is_default +FROM account_addresses +WHERE account_id = $1 AND is_default + "#, + ) + .bind(msg.account_id) + .fetch_one(pool) + .await + .map_err(|_| Error::AccountAddresses.into()) +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] pub struct CreateAccountAddress { pub name: model::Name, pub email: model::Email, @@ -47,25 +77,41 @@ pub struct CreateAccountAddress { pub city: model::City, pub country: model::Country, pub zip: model::Zip, - pub account_id: model::AccountId, + pub account_id: Option, + pub is_default: bool, } db_async_handler!( CreateAccountAddress, create_address, - model::Address, + model::AccountAddress, inner_create_address ); pub(crate) async fn create_address( msg: CreateAccountAddress, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, -) -> Result { +) -> Result { + if msg.is_default { + if let Err(e) = sqlx::query( + r#" +UPDATE account_addresses +SET is_default = FALSE +WHERE account_id = $1 + "#, + ) + .bind(msg.account_id) + .fetch_all(&mut *pool) + .await + { + log::error!("{}", e); + } + } sqlx::query_as( r#" INSERT INTO account_addresses ( name, email, street, city, country, zip, account_id ) VALUES ($1, $2, $3, $4, $5, $6, $7) -RETURNING id, name, email, street, city, country, zip, account_id +RETURNING id, name, email, street, city, country, zip, account_id, is_default "#, ) .bind(msg.name) @@ -81,7 +127,7 @@ RETURNING id, name, email, street, city, country, zip, account_id } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] pub struct UpdateAccountAddress { pub id: model::AddressId, pub name: model::Name, @@ -91,25 +137,26 @@ pub struct UpdateAccountAddress { pub country: model::Country, pub zip: model::Zip, pub account_id: model::AccountId, + pub is_default: bool, } db_async_handler!( UpdateAccountAddress, update_account_address, - model::Address, + model::AccountAddress, inner_update_account_address ); pub(crate) async fn update_account_address( msg: UpdateAccountAddress, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, -) -> Result { +) -> Result { sqlx::query_as( r#" UPDATE account_addresses -SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7, account_id = $8 +SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7, account_id = $8, is_default = $9 WHERE id = $1 -RETURNING id, name, email, street, city, country, zip, account_id +RETURNING id, name, email, street, city, country, zip, account_id, is_default "#, ) .bind(msg.id) @@ -120,6 +167,7 @@ RETURNING id, name, email, street, city, country, zip, account_id .bind(msg.country) .bind(msg.zip) .bind(msg.account_id) + .bind(msg.is_default) .fetch_one(pool) .await .map_err(|_| Error::CreateAccountAddress.into()) diff --git a/actors/database_manager/src/lib.rs b/actors/database_manager/src/lib.rs index ae6221d..13a18fe 100644 --- a/actors/database_manager/src/lib.rs +++ b/actors/database_manager/src/lib.rs @@ -3,10 +3,11 @@ use config::SharedAppConfig; use sqlx::PgPool; use sqlx_core::arguments::Arguments; -pub use crate::account_orders::*; +pub use crate::account_addresses::*; pub use crate::accounts::*; -pub use crate::addresses::*; +pub use crate::order_addresses::*; pub use crate::order_items::*; +pub use crate::orders::*; pub use crate::photos::*; pub use crate::product_photos::*; pub use crate::products::*; @@ -15,10 +16,11 @@ pub use crate::shopping_carts::*; pub use crate::stocks::*; pub use crate::tokens::*; -pub mod account_orders; +pub mod account_addresses; pub mod accounts; -pub mod addresses; +pub mod order_addresses; pub mod order_items; +pub mod orders; pub mod photos; pub mod product_photos; pub mod products; @@ -128,7 +130,7 @@ pub enum Error { #[error("{0}")] Account(#[from] accounts::Error), #[error("{0}")] - AccountOrder(#[from] account_orders::Error), + AccountOrder(#[from] orders::Error), #[error("{0}")] Product(#[from] products::Error), #[error("{0}")] @@ -146,7 +148,9 @@ pub enum Error { #[error("{0}")] ProductPhoto(#[from] product_photos::Error), #[error("{0}")] - AccountAddress(#[from] addresses::Error), + AccountAddress(#[from] account_addresses::Error), + #[error("{0}")] + OrderAddress(#[from] order_addresses::Error), #[error("Failed to start or finish transaction")] TransactionFailed, } diff --git a/actors/database_manager/src/order_addresses.rs b/actors/database_manager/src/order_addresses.rs new file mode 100644 index 0000000..fd59479 --- /dev/null +++ b/actors/database_manager/src/order_addresses.rs @@ -0,0 +1,130 @@ +use crate::{db_async_handler, Result}; + +#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Can't load account addresses")] + OrderAddress, + #[error("Failed to save account address")] + CreateOrderAddress, +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct OrderAddress { + pub order_id: model::OrderId, +} + +db_async_handler!( + OrderAddress, + order_address, + model::OrderAddress, + inner_order_address +); + +pub(crate) async fn order_address( + msg: OrderAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +SELECT + order_addresses.id, + order_addresses.name, + order_addresses.email, + order_addresses.street, + order_addresses.city, + order_addresses.country, + order_addresses.zip +FROM order_addresses +INNER JOIN orders ON orders.address_id = order_addresses.id +WHERE orders.id = $1 + "#, + ) + .bind(msg.order_id) + .fetch_one(pool) + .await + .map_err(|_| Error::OrderAddress.into()) +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct CreateOrderAddress { + pub name: model::Name, + pub email: model::Email, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, +} + +db_async_handler!( + CreateOrderAddress, + create_order_address, + model::OrderAddress, + inner_create_order_address +); + +pub(crate) async fn create_order_address( + msg: CreateOrderAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +INSERT INTO order_addresses ( name, email, street, city, country, zip ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, name, email, street, city, country, zip + "#, + ) + .bind(msg.name) + .bind(msg.email) + .bind(msg.street) + .bind(msg.city) + .bind(msg.country) + .bind(msg.zip) + .fetch_one(pool) + .await + .map_err(|_| Error::CreateOrderAddress.into()) +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct UpdateOrderAddress { + pub id: model::OrderAddressId, + pub name: model::Name, + pub email: model::Email, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, +} + +db_async_handler!( + UpdateOrderAddress, + update_account_address, + model::OrderAddress, + inner_update_account_address +); + +pub(crate) async fn update_account_address( + msg: UpdateOrderAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +UPDATE order_addresses +SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7 +WHERE id = $1 +RETURNING id, name, email, street, city, country, zip + "#, + ) + .bind(msg.id) + .bind(msg.name) + .bind(msg.email) + .bind(msg.street) + .bind(msg.city) + .bind(msg.country) + .bind(msg.zip) + .fetch_one(pool) + .await + .map_err(|_| Error::CreateOrderAddress.into()) +} diff --git a/actors/database_manager/src/order_items.rs b/actors/database_manager/src/order_items.rs index 5ebfda6..5b12080 100644 --- a/actors/database_manager/src/order_items.rs +++ b/actors/database_manager/src/order_items.rs @@ -47,7 +47,7 @@ ORDER BY id DESC #[rtype(result = "Result")] pub struct CreateOrderItem { pub product_id: ProductId, - pub order_id: AccountOrderId, + pub order_id: OrderId, pub quantity: Quantity, pub quantity_unit: QuantityUnit, } @@ -109,7 +109,7 @@ WHERE id = $1 #[derive(actix::Message)] #[rtype(result = "Result>")] pub struct OrderItems { - pub order_id: model::AccountOrderId, + pub order_id: model::OrderId, } db_async_handler!(OrderItems, order_items, Vec); diff --git a/actors/database_manager/src/account_orders.rs b/actors/database_manager/src/orders.rs similarity index 75% rename from actors/database_manager/src/account_orders.rs rename to actors/database_manager/src/orders.rs index 080d635..3b5888b 100644 --- a/actors/database_manager/src/account_orders.rs +++ b/actors/database_manager/src/orders.rs @@ -21,19 +21,16 @@ pub enum Error { } #[derive(actix::Message)] -#[rtype(result = "Result>")] +#[rtype(result = "Result>")] pub struct AllAccountOrders; -db_async_handler!(AllAccountOrders, all_account_orders, Vec); +db_async_handler!(AllAccountOrders, all_orders, Vec); -pub(crate) async fn all_account_orders( - _msg: AllAccountOrders, - pool: PgPool, -) -> Result> { +pub(crate) async fn all_orders(_msg: AllAccountOrders, pool: PgPool) -> Result> { sqlx::query_as( r#" -SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes -FROM account_orders +SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id +FROM orders ORDER BY id DESC "#, ) @@ -56,7 +53,7 @@ pub mod create_order { } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] pub struct CreateAccountOrder { pub buyer_id: AccountId, pub items: Vec, @@ -67,19 +64,19 @@ pub struct CreateAccountOrder { db_async_handler!( CreateAccountOrder, create_account_order, - AccountOrder, + Order, inner_create_account_order ); pub(crate) async fn create_account_order( msg: CreateAccountOrder, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, -) -> Result { - let order: AccountOrder = match sqlx::query_as( +) -> Result { + let order: Order = match sqlx::query_as( r#" -INSERT INTO account_orders (buyer_id, status) +INSERT INTO orders (buyer_id, status) VALUES ($1, $2, $3) -RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.buyer_id) @@ -130,26 +127,23 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] pub struct UpdateAccountOrder { - pub id: AccountOrderId, + pub id: OrderId, pub buyer_id: AccountId, pub status: OrderStatus, - pub order_id: Option, + pub order_id: Option, } -db_async_handler!(UpdateAccountOrder, update_account_order, AccountOrder); +db_async_handler!(UpdateAccountOrder, update_account_order, Order); -pub(crate) async fn update_account_order( - msg: UpdateAccountOrder, - db: PgPool, -) -> Result { +pub(crate) async fn update_account_order(msg: UpdateAccountOrder, db: PgPool) -> Result { sqlx::query_as( r#" -UPDATE account_orders +UPDATE orders SET buyer_id = $2 AND status = $3 AND order_id = $4 WHERE id = $1 -RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.id) @@ -165,28 +159,24 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] pub struct UpdateAccountOrderByExt { pub order_ext_id: String, pub status: OrderStatus, } -db_async_handler!( - UpdateAccountOrderByExt, - update_account_order_by_ext, - AccountOrder -); +db_async_handler!(UpdateAccountOrderByExt, update_account_order_by_ext, Order); pub(crate) async fn update_account_order_by_ext( msg: UpdateAccountOrderByExt, db: PgPool, -) -> Result { +) -> Result { sqlx::query_as( r#" -UPDATE account_orders +UPDATE orders SET status = $2 WHERE order_ext_id = $1 -RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.order_ext_id) @@ -200,18 +190,18 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] pub struct FindAccountOrder { - pub id: AccountOrderId, + pub id: OrderId, } -db_async_handler!(FindAccountOrder, find_account_order, AccountOrder); +db_async_handler!(FindAccountOrder, find_account_order, Order); -pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result { +pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result { sqlx::query_as( r#" -SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes -FROM account_orders +SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id +FROM orders WHERE id = $1 "#, ) @@ -225,24 +215,21 @@ WHERE id = $1 } #[derive(actix::Message)] -#[rtype(result = "Result")] +#[rtype(result = "Result")] pub struct SetOrderServiceId { - pub id: AccountOrderId, + pub id: OrderId, pub service_order_id: String, } -db_async_handler!(SetOrderServiceId, set_order_service_id, AccountOrder); +db_async_handler!(SetOrderServiceId, set_order_service_id, Order); -pub(crate) async fn set_order_service_id( - msg: SetOrderServiceId, - db: PgPool, -) -> Result { +pub(crate) async fn set_order_service_id(msg: SetOrderServiceId, db: PgPool) -> Result { sqlx::query_as( r#" -UPDATE account_orders +UPDATE orders SET service_order_id = $2 WHERE id = $1 -RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.id) diff --git a/actors/order_manager/src/lib.rs b/actors/order_manager/src/lib.rs index 9725059..8dc019d 100644 --- a/actors/order_manager/src/lib.rs +++ b/actors/order_manager/src/lib.rs @@ -1,7 +1,7 @@ use actix::Message; use config::SharedAppConfig; use database_manager::{query_db, SharedDatabase}; -use model::{AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem}; +use model::{AccountId, Order, OrderStatus, ShoppingCart, ShoppingCartItem}; #[macro_export] macro_rules! order_async_handler { @@ -19,6 +19,27 @@ macro_rules! order_async_handler { }; } +#[macro_export] +macro_rules! query_order { + ($order_manager: expr, $msg: expr, $fail: expr) => { + $crate::query_order!($order_manager, $msg, $fail, $fail) + }; + + ($order_manager: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => { + match $order_manager.send($msg).await { + Ok(Ok(r)) => Ok(r), + Ok(Err(e)) => { + log::error!("{e}"); + Err($db_fail) + } + Err(e) => { + log::error!("{e:?}"); + Err($act_fail) + } + } + }; +} + #[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] #[serde(rename_all = "kebab-case", tag = "order")] pub enum Error { @@ -28,6 +49,12 @@ pub enum Error { ShoppingCart, #[error("Failed to create account order")] CreateAccountOrder, + #[error("Account does not have address")] + NoAddress, + #[error("Invalid account address")] + InvalidAccountAddress, + #[error("Invalid order address")] + InvalidOrderAddress, } pub type Result = std::result::Result; @@ -47,24 +74,34 @@ impl OrderManager { } } -#[derive(Message, Debug)] -#[rtype(result = "Result")] -pub struct CreateOrder { - pub account_id: AccountId, - pub shopping_cart_id: ShoppingCartId, +#[derive(Debug)] +pub struct CreateOrderAddress { + pub name: model::Name, + pub email: model::Email, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, } -order_async_handler!(CreateOrder, create_order, AccountOrder); +#[derive(Message, Debug)] +#[rtype(result = "Result")] +pub struct CreateAccountOrder { + pub account_id: AccountId, + pub create_address: Option, +} -pub(crate) async fn create_order( - msg: CreateOrder, +order_async_handler!(CreateAccountOrder, create_account_order, Order); + +pub(crate) async fn create_account_order( + msg: CreateAccountOrder, db: SharedDatabase, _config: SharedAppConfig, -) -> Result { +) -> Result { let cart: ShoppingCart = query_db!( db, - database_manager::FindShoppingCart { - id: msg.shopping_cart_id, + database_manager::EnsureActiveShoppingCart { + buyer_id: msg.account_id }, Error::ShoppingCart, Error::DatabaseInternal @@ -78,6 +115,44 @@ pub(crate) async fn create_order( Error::ShoppingCart, Error::DatabaseInternal ); + let address: model::AccountAddress = if let Some(input) = msg.create_address { + query_db!( + db, + database_manager::CreateAccountAddress { + name: input.name, + email: input.email, + street: input.street, + city: input.city, + country: input.country, + zip: input.zip, + account_id: None, + is_default: true, + }, + Error::InvalidAccountAddress + ) + } else { + query_db!( + db, + database_manager::DefaultAccountAddress { + account_id: cart.buyer_id + }, + Error::NoAddress + ) + }; + + query_db!( + db, + database_manager::CreateOrderAddress { + name: address.name, + email: address.email, + street: address.street, + city: address.city, + country: address.country, + zip: address.zip, + }, + Error::InvalidOrderAddress + ); + let order = query_db!( db, database_manager::CreateAccountOrder { diff --git a/actors/payment_manager/src/lib.rs b/actors/payment_manager/src/lib.rs index 22cb0ea..b304d41 100644 --- a/actors/payment_manager/src/lib.rs +++ b/actors/payment_manager/src/lib.rs @@ -151,7 +151,7 @@ impl From for pay_u::Product { } pub struct CreatePaymentResult { - pub order: model::AccountOrder, + pub order: model::Order, pub items: Vec, pub redirect_uri: String, } @@ -215,7 +215,7 @@ pub(crate) async fn request_payment( Error::UnavailableShoppingCart ); - let db_order: model::AccountOrder = query_db!( + let db_order: model::Order = query_db!( db, database_manager::CreateAccountOrder { buyer_id: msg.buyer_id, diff --git a/api/src/routes/admin/api_v1/accounts.rs b/api/src/routes/admin/api_v1/accounts.rs index 0c574aa..e7e4198 100644 --- a/api/src/routes/admin/api_v1/accounts.rs +++ b/api/src/routes/admin/api_v1/accounts.rs @@ -4,7 +4,7 @@ use actix_web::{get, patch, post, HttpResponse}; use actix_web_httpauth::extractors::bearer::BearerAuth; use config::SharedAppConfig; use database_manager::Database; -use model::{AccountId, AccountState, Address, Encrypt, PasswordConfirmation}; +use model::{AccountAddress, AccountId, AccountState, Encrypt, PasswordConfirmation}; use token_manager::TokenManager; use crate::routes::admin::Error; @@ -112,7 +112,7 @@ pub async fn create_account( role: payload.role, } ); - let addresses: Vec
= admin_send_db!( + let addresses: Vec = admin_send_db!( db, database_manager::AccountAddresses { account_id: account.id diff --git a/api/src/routes/admin/api_v1/orders.rs b/api/src/routes/admin/api_v1/orders.rs index a1d7997..2f3a3e9 100644 --- a/api/src/routes/admin/api_v1/orders.rs +++ b/api/src/routes/admin/api_v1/orders.rs @@ -3,7 +3,7 @@ use actix_web::get; use actix_web::web::{Data, Json, ServiceConfig}; use actix_web_httpauth::extractors::bearer::BearerAuth; use database_manager::Database; -use model::api::AccountOrders; +use model::api::Orders; use token_manager::TokenManager; use crate::routes::RequireUser; @@ -14,10 +14,10 @@ async fn orders( credentials: BearerAuth, tm: Data>, db: Data>, -) -> routes::Result> { +) -> routes::Result> { credentials.require_admin(tm.into_inner()).await?; - let orders: Vec = admin_send_db!(&db, database_manager::AllAccountOrders); + let orders: Vec = admin_send_db!(&db, database_manager::AllAccountOrders); let items: Vec = admin_send_db!(db, database_manager::AllOrderItems); Ok(Json((orders, items).into())) diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index 9277b3a..8a57bf5 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -6,6 +6,7 @@ use actix_web_httpauth::extractors::bearer::BearerAuth; use cart_manager::{query_cart, CartManager}; use database_manager::{query_db, Database}; use model::api; +use order_manager::{query_order, OrderManager}; use payment_manager::{query_pay, PaymentManager}; use token_manager::TokenManager; @@ -234,6 +235,7 @@ pub(crate) async fn create_order( tm: Data>, credentials: BearerAuth, payment: Data>, + order: Data>, ) -> routes::Result { let account_id = credentials .require_user(tm.into_inner()) @@ -248,6 +250,7 @@ pub(crate) async fn create_order( language, charge_client, currency, + address, } = payload; let ip = match req.peer_addr() { Some(ip) => ip, @@ -272,6 +275,32 @@ pub(crate) async fn create_order( routes::Error::Public(PublicError::DatabaseConnection) ); + query_order!( + order, + order_manager::CreateAccountOrder { + account_id, + create_address: address.map( + |model::api::CreateOrderAddress { + name, + email, + street, + city, + country, + zip, + }| order_manager::CreateOrderAddress { + name, + email, + street, + city, + country, + zip, + }, + ), + }, + PublicError::PlaceOrder, + PublicError::DatabaseConnection + )?; + Ok(HttpResponse::SeeOther() .append_header(("Location", redirect_uri.as_str())) .body(format!( diff --git a/api/src/routes/public/mod.rs b/api/src/routes/public/mod.rs index 34db43d..5de9ad5 100644 --- a/api/src/routes/public/mod.rs +++ b/api/src/routes/public/mod.rs @@ -48,6 +48,8 @@ macro_rules! public_send_db { pub enum Error { #[error("{0}")] ApiV1(#[from] api_v1::Error), + #[error("Failed to place order")] + PlaceOrder, #[error("Internal server error")] DatabaseConnection, #[error("{0}")] diff --git a/migrations/20220523090806_change_orders.sql b/migrations/20220523090806_change_orders.sql new file mode 100644 index 0000000..e43cb88 --- /dev/null +++ b/migrations/20220523090806_change_orders.sql @@ -0,0 +1,19 @@ +ALTER TABLE account_orders + ALTER COLUMN buyer_id DROP NOT NULL; + +CREATE TABLE order_addresses +( + id serial not null primary key unique, + name text not null, + email text not null, + street text not null, + city text not null, + country text not null, + zip text not null +); + +ALTER TABLE account_orders + ADD COLUMN address_id INT REFERENCES order_addresses (id); + +ALTER TABLE account_orders + RENAME TO orders; diff --git a/migrations/20220523095745_add_default_to_account_address.sql b/migrations/20220523095745_add_default_to_account_address.sql new file mode 100644 index 0000000..9c52cf3 --- /dev/null +++ b/migrations/20220523095745_add_default_to_account_address.sql @@ -0,0 +1,2 @@ +ALTER TABLE account_addresses + ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT false; diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index 3a61a28..e1d9177 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -28,10 +28,10 @@ pub struct Account { pub role: Role, pub customer_id: uuid::Uuid, pub state: AccountState, - pub addresses: Vec
, + pub addresses: Vec, } -impl From<(FullAccount, Vec)> for Account { +impl From<(FullAccount, Vec)> for Account { fn from( ( FullAccount { @@ -44,7 +44,7 @@ impl From<(FullAccount, Vec)> for Account { state, }, addresses, - ): (FullAccount, Vec), + ): (FullAccount, Vec), ) -> Self { Self { id, @@ -59,7 +59,7 @@ impl From<(FullAccount, Vec)> for Account { } #[derive(Serialize, Deserialize, Debug)] -pub struct Address { +pub struct AccountAddress { pub id: AddressId, pub name: Name, pub email: Email, @@ -68,11 +68,12 @@ pub struct Address { pub country: Country, pub zip: Zip, pub account_id: AccountId, + pub is_default: bool, } -impl From for Address { +impl From for AccountAddress { fn from( - crate::Address { + crate::AccountAddress { id, name, email, @@ -81,7 +82,8 @@ impl From for Address { country, zip, account_id, - }: crate::Address, + is_default, + }: crate::AccountAddress, ) -> Self { Self { id, @@ -92,6 +94,7 @@ impl From for Address { country, zip, account_id, + is_default, } } } @@ -99,15 +102,15 @@ impl From for Address { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[derive(Serialize, Deserialize, Debug)] #[serde(transparent)] -pub struct AccountOrders(pub Vec); +pub struct Orders(pub Vec); -impl From<(Vec, Vec)> for AccountOrders { - fn from((orders, mut items): (Vec, Vec)) -> Self { +impl From<(Vec, Vec)> for Orders { + fn from((orders, mut items): (Vec, Vec)) -> Self { Self( orders .into_iter() .map( - |crate::AccountOrder { + |crate::Order { id, buyer_id, status, @@ -115,14 +118,16 @@ impl From<(Vec, Vec)> for AccountOrders { order_ext_id: _, service_order_id: _, checkout_notes, + address_id, }| { - AccountOrder { + Order { id, buyer_id, status, order_id, items: items.drain_filter(|item| item.order_id == id).collect(), checkout_notes, + address_id, } }, ) @@ -131,10 +136,10 @@ impl From<(Vec, Vec)> for AccountOrders { } } -impl From<(crate::AccountOrder, Vec)> for AccountOrder { +impl From<(crate::Order, Vec)> for Order { fn from( ( - crate::AccountOrder { + crate::Order { id, buyer_id, status, @@ -142,30 +147,33 @@ impl From<(crate::AccountOrder, Vec)> for AccountOrder { order_ext_id: _, service_order_id: _, checkout_notes, + address_id, }, mut items, - ): (crate::AccountOrder, Vec), + ): (crate::Order, Vec), ) -> Self { - AccountOrder { + Order { id, buyer_id, status, order_id, items: items.drain_filter(|item| item.order_id == id).collect(), checkout_notes, + address_id, } } } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[derive(Serialize, Deserialize, Debug)] -pub struct AccountOrder { - pub id: crate::AccountOrderId, +pub struct Order { + pub id: crate::OrderId, pub buyer_id: crate::AccountId, pub status: crate::OrderStatus, - pub order_id: Option, + pub order_id: Option, pub items: Vec, pub checkout_notes: Option, + pub address_id: OrderAddressId, } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] @@ -436,6 +444,7 @@ pub struct CreateOrderInput { pub charge_client: bool, /// User currency pub currency: String, + pub address: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -482,6 +491,27 @@ pub struct SearchRequest { pub lang: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct CreateOrderAddress { + pub name: Name, + pub email: Email, + pub street: Street, + pub city: City, + pub country: Country, + pub zip: Zip, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct UpdateOrderAddress { + pub id: OrderAddressId, + pub name: Name, + pub email: Email, + pub street: Street, + pub city: City, + pub country: Country, + pub zip: Zip, +} + pub mod admin { use serde::{Deserialize, Serialize}; diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 9d1dc21..1002a61 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -342,7 +342,7 @@ impl Login { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[derive(Serialize, Debug, Clone, Default, Deref, DerefMut, From, Display)] #[serde(transparent)] pub struct Email(String); @@ -843,42 +843,51 @@ pub struct Stock { #[cfg_attr(feature = "db", sqlx(transparent))] #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)] #[serde(transparent)] -pub struct AccountOrderId(RecordId); +pub struct OrderAddressId(RecordId); + +#[cfg_attr(feature = "dummy", derive(fake::Dummy))] +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)] +#[serde(transparent)] +pub struct OrderId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Display, Deref)] #[serde(transparent)] -pub struct OrderId(String); +pub struct ExtOrderId(String); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[derive(Serialize, Deserialize)] -pub struct AccountOrder { - pub id: AccountOrderId, +pub struct Order { + pub id: OrderId, pub buyer_id: AccountId, pub status: OrderStatus, - pub order_id: Option, + pub order_id: Option, pub order_ext_id: uuid::Uuid, pub service_order_id: Option, pub checkout_notes: Option, + pub address_id: OrderAddressId, } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[derive(Serialize, Deserialize)] pub struct PublicAccountOrder { - pub id: AccountOrderId, + pub id: OrderId, pub buyer_id: AccountId, pub status: OrderStatus, - pub order_id: Option, + pub order_id: Option, pub checkout_notes: String, + pub address_id: OrderAddressId, } -impl From for PublicAccountOrder { +impl From for PublicAccountOrder { fn from( - AccountOrder { + Order { id, buyer_id, status, @@ -886,7 +895,8 @@ impl From for PublicAccountOrder { order_ext_id: _, service_order_id: _, checkout_notes, - }: AccountOrder, + address_id, + }: Order, ) -> Self { Self { id, @@ -894,6 +904,7 @@ impl From for PublicAccountOrder { status, order_id, checkout_notes: checkout_notes.unwrap_or_default(), + address_id, } } } @@ -910,7 +921,7 @@ pub struct OrderItemId(pub RecordId); pub struct OrderItem { pub id: OrderItemId, pub product_id: ProductId, - pub order_id: AccountOrderId, + pub order_id: OrderId, pub quantity: Quantity, pub quantity_unit: QuantityUnit, } @@ -1128,7 +1139,7 @@ pub struct AddressId(RecordId); #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)] #[serde(transparent)] pub struct Name(String); @@ -1140,7 +1151,7 @@ impl Name { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)] #[serde(transparent)] pub struct Street(String); @@ -1152,7 +1163,7 @@ impl Street { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)] #[serde(transparent)] pub struct City(String); @@ -1164,7 +1175,7 @@ impl City { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)] #[serde(transparent)] pub struct Country(String); @@ -1176,7 +1187,7 @@ impl Country { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)] #[serde(transparent)] pub struct Zip(String); @@ -1189,7 +1200,7 @@ impl Zip { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[derive(Serialize, Deserialize, Debug)] -pub struct Address { +pub struct AccountAddress { pub id: AddressId, pub name: Name, pub email: Email, @@ -1198,4 +1209,18 @@ pub struct Address { pub country: Country, pub zip: Zip, pub account_id: AccountId, + pub is_default: bool, +} + +#[cfg_attr(feature = "dummy", derive(fake::Dummy))] +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize, Debug)] +pub struct OrderAddress { + pub id: OrderAddressId, + pub name: Name, + pub email: Email, + pub street: Street, + pub city: City, + pub country: Country, + pub zip: Zip, } diff --git a/web/src/api/public.rs b/web/src/api/public.rs index 55bdb88..20b00d7 100644 --- a/web/src/api/public.rs +++ b/web/src/api/public.rs @@ -117,3 +117,24 @@ pub async fn update_cart( ) .await } + +pub async fn place_order(access_token: AccessTokenString) -> NetRes { + let input = model::api::CreateOrderInput { + email: "".to_string(), + phone: "".to_string(), + first_name: "".to_string(), + last_name: "".to_string(), + language: "".to_string(), + charge_client: false, + currency: "".to_string(), + address: None, + }; + perform( + Request::new("/api/v1/order") + .method(Method::Post) + .header(Header::bearer(access_token.as_str())) + .json(&input) + .map_err(NetRes::Http)?, + ) + .await +} diff --git a/web/src/pages/public/checkout.rs b/web/src/pages/public/checkout.rs index 01a45d3..c3f920f 100644 --- a/web/src/pages/public/checkout.rs +++ b/web/src/pages/public/checkout.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use seed::prelude::*; use seed::*; @@ -7,11 +9,28 @@ use crate::NetRes; #[derive(Debug)] pub enum CheckoutMsg { ProductsFetched(NetRes), + AddressNameChanged(String), + AddressEmailChanged(String), + AddressStreetChanged(String), + AddressCityChanged(String), + AddressCountryChanged(String), + AddressZipChanged(String), +} + +#[derive(Debug, Default)] +pub struct AddressForm { + pub name: model::Name, + pub email: model::Email, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, } #[derive(Debug)] pub struct CheckoutPage { pub products: Products, + pub address: AddressForm, } pub fn init(_url: Url, orders: &mut impl Orders) -> CheckoutPage { @@ -22,6 +41,7 @@ pub fn init(_url: Url, orders: &mut impl Orders) -> CheckoutPage { }); CheckoutPage { products: Default::default(), + address: AddressForm::default(), } } @@ -38,6 +58,26 @@ pub fn update(msg: CheckoutMsg, model: &mut CheckoutPage, _orders: &mut impl Ord CheckoutMsg::ProductsFetched(NetRes::Http(e)) => { seed::error!("fetch product http", e); } + CheckoutMsg::AddressNameChanged(value) => { + model.address.name = model::Name::new(value); + } + CheckoutMsg::AddressEmailChanged(value) => { + if let Ok(value) = model::Email::from_str(&value) { + model.address.email = value; + } + } + CheckoutMsg::AddressStreetChanged(value) => { + model.address.street = model::Street::new(value); + } + CheckoutMsg::AddressCityChanged(value) => { + model.address.city = model::City::new(value); + } + CheckoutMsg::AddressCountryChanged(value) => { + model.address.country = model::Country::new(value); + } + CheckoutMsg::AddressZipChanged(value) => { + model.address.zip = model::Zip::new(value); + } } } @@ -182,7 +222,8 @@ mod right_side { use seed::prelude::*; use seed::*; - use crate::pages::public::checkout::CheckoutPage; + use crate::pages::public::checkout::{CheckoutMsg, CheckoutPage}; + use crate::pages::public::sign_up::RegisterMsg; use crate::shopping_cart::CartMsg; use crate::Msg; @@ -204,11 +245,15 @@ mod right_side { ] } - fn contact(model: &crate::Model, _page: &CheckoutPage) -> Node { - if model.shared.me.is_some() { - // TODO: Display user addresses - return empty![]; + fn contact(model: &crate::Model, page: &CheckoutPage) -> Node { + match &model.shared.me { + Some(me) if me.addresses.is_empty() => contact_form(model, page), + Some(_me) => empty![], + None => contact_form(model, page), } + } + + fn contact_form(model: &crate::Model, _page: &CheckoutPage) -> Node { div![ C!["w-full mx-auto rounded-lg bg-white border border-gray-200 p-3 text-gray-800 font-light mb-6"], form![ @@ -219,19 +264,13 @@ mod right_side { 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")] - ] + address_input(model, "client-name", "text", "Name", CheckoutMsg::AddressNameChanged), ], ], 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")] - ] + address_input(model, "client-email", "email", "E-Mail", CheckoutMsg::AddressEmailChanged), ], ], div![ @@ -240,36 +279,47 @@ mod right_side { ], 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")] - ] + address_input(model, "client-street", "text", "Street", CheckoutMsg::AddressStreetChanged), ], 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")] - ] + address_input(model, "client-city", "text", "City", CheckoutMsg::AddressCityChanged), ], 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")] - ] + address_input(model, "client-country", "text", "Country", CheckoutMsg::AddressCountryChanged), ], 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")] - ] + address_input(model, "client-zip", "text", "Zip", CheckoutMsg::AddressZipChanged), ], ] ] } + fn address_input( + model: &crate::Model, + id: &str, + ty: &str, + label: &'static str, + msg: F, + ) -> Node + where + F: Clone + 'static + Fn(String) -> CheckoutMsg, + { + let handler = ev(Ev::Change, move |ev| { + ev.prevent_default(); + let target = ev.target()?; + let input = seed::to_input(&target); + Some(crate::Msg::from(msg(input.value()))) + }); + 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 => id, At::Type => ty, At::Placeholder => model.i18n.t(label), At::Required => true], + handler, + ] + } + fn pay_now(model: &crate::Model) -> Node { div![ button![ @@ -292,13 +342,7 @@ mod right_side { } fn pay_u(model: &crate::Model) -> Node { - payment_input( - model, - model::PaymentMethod::PayU, - "pay_u", - "PayU", - pay_u_icon(), - ) + payment_input(model, PaymentMethod::PayU, "pay_u", "PayU", pay_u_icon()) } fn pay_u_icon() -> Node { @@ -329,7 +373,7 @@ mod right_side { fn pay_on_spot(model: &crate::Model) -> Node { payment_input( model, - model::PaymentMethod::PaymentOnTheSpot, + PaymentMethod::PaymentOnTheSpot, "pay_on_spot", "Pay on spot", pay_in_spot_icon(), @@ -397,7 +441,7 @@ mod right_side { fn payment_input( model: &crate::Model, - method: model::PaymentMethod, + method: PaymentMethod, name: &str, label: &'static str, icon: Node, diff --git a/web/src/pages/public/sign_up.rs b/web/src/pages/public/sign_up.rs index d9040a9..2b2487b 100644 --- a/web/src/pages/public/sign_up.rs +++ b/web/src/pages/public/sign_up.rs @@ -56,7 +56,6 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders let email = model.email.clone(); let login = model.login.clone(); let password = model.password.clone(); - let password_confirmation = model.password_confirmation.clone(); orders.perform_cmd(async move { crate::Msg::Public( @@ -65,7 +64,6 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders email, login, password, - password_confirmation, }) .await, ) @@ -84,16 +82,15 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders pub fn view(model: &crate::Model, page: &SignUpPage) -> Node { let home = Urls::new(&model.url).home(); - let logo = model - .logo - .as_deref() - .map(|src| { + let logo = model.logo.as_deref().map_or_else( + || a![attrs![At::Href => home], "Logo"], + |src| { a![ attrs![At::Href => home], img![attrs![At::Src => src], C!["m-auto"]] ] - }) - .unwrap_or_else(|| a![attrs![At::Href => home], "Logo"]); + }, + ); let content = div![ C!["relative flex flex-col justify-center overflow-hidden"], @@ -129,51 +126,54 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node { RegisterMsg::Submit }), div![ - label![attrs!["for" => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")], + label![attrs![At::For => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")], input![ - attrs!["type" => "email", "id" => "email"], + attrs![At::Type => "email", At::Id => "email"], 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(RegisterMsg::EmailChanged) - }) + }), ] ], div![ - label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")], + label![attrs![At::For => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")], input![ - attrs!["type" => "text", "id" => "login"], + attrs![At::Type => "text", At::Id => "login"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"], ev(Ev::Change, |ev| { ev.stop_propagation(); ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::LoginChanged) - }) + }), ] ], div![ C!["mt-4"], div![ - label![attrs!["for" => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")], - input![attrs!["type" => "password", "id" => "password"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]], - ev(Ev::Change, |ev| { - ev.stop_propagation(); - ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordChanged) - }) + label![attrs![At::For => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")], + input![ + attrs![At::Type => "password", At::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(RegisterMsg::PasswordChanged) + }) + ] ], div![ label![ - attrs!["for" => "password-confirmation"], + attrs![At::For => "password-confirmation"], C!["block text-sm text-indigo-800"], model.i18n.t("Password confirmation") ], input![ - attrs!["type" => "password", "id" => "password-confirmation"], - 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(RegisterMsg::PasswordConfirmationChanged) - }) + attrs![At::Type => "password", At::Id => "password-confirmation"], + 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(RegisterMsg::PasswordConfirmationChanged) + }), + ] ], div![ C!["mt-6"],