diff --git a/.env b/.env index 9bdab2b..eaafd74 100644 --- a/.env +++ b/.env @@ -1,4 +1,7 @@ +DATABASE_NAME=bazzar DATABASE_URL=postgres://postgres@localhost/bazzar +ACCOUNT_DATABASE_URL=postgres://postgres@localhost/bazzar_accounts +CART_DATABASE_URL=postgres://postgres@localhost/bazzar_carts PASS_SALT=18CHwV7eGFAea16z+qMKZg RUST_LOG=debug diff --git a/Cargo.lock b/Cargo.lock index ba2c279..d64cbfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,7 +12,6 @@ dependencies = [ "bytes", "channels", "config", - "database_manager", "dotenv", "futures 0.3.25", "gumdrop", @@ -23,6 +22,8 @@ dependencies = [ "pretty_env_logger", "rumqttc", "serde", + "sqlx", + "sqlx-core", "tarpc", "thiserror", "tokio", @@ -836,13 +837,22 @@ dependencies = [ "channels", "chrono", "config", - "database_manager", + "dotenv", + "futures 0.3.25", "model", + "opentelemetry 0.17.0", + "opentelemetry-jaeger", "pretty_env_logger", "rumqttc", "serde", + "sqlx", + "sqlx-core", + "tarpc", "thiserror", + "tokio", "tracing", + "tracing-opentelemetry", + "tracing-subscriber", "uuid 0.8.2", ] diff --git a/Cargo.toml b/Cargo.toml index 34cb6af..0bf2ccf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,25 @@ [workspace] members = [ # shared - "shared/model", - "shared/channels", - "shared/config", - "shared/testx", + "crates/model", + "crates/channels", + "crates/config", + "crates/testx", # actors - "actors/account_manager", - "actors/cart_manager", - "actors/database_manager", - "actors/email_manager", - "actors/order_manager", - "actors/payment_manager", - "actors/search_manager", - "actors/token_manager", - "actors/fs_manager", - "actors/lang_provider", + "crates/account_manager", + "crates/cart_manager", + "crates/database_manager", + "crates/email_manager", + "crates/order_manager", + "crates/payment_manager", + "crates/search_manager", + "crates/token_manager", + "crates/fs_manager", + "crates/lang_provider", # artifacts - "db-seed", - "api", - "web", + "crates/db-seed", + "crates/api", + "crates/web", # vendor "vendor/t_pay", ] diff --git a/actors/account_manager/src/mqtt.rs b/actors/account_manager/src/mqtt.rs deleted file mode 100644 index b77d9c5..0000000 --- a/actors/account_manager/src/mqtt.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::time::Duration; - -use channels::account::{AccountFailure, CreateAccount, Topic}; -use config::SharedAppConfig; -use database_manager::Database; -use rumqttc::{AsyncClient, Event, Incoming, MqttOptions, QoS}; - -use crate::{actions, Error}; - -pub async fn start(config: SharedAppConfig, db: Database) -> channels::AsyncClient { - tracing::info!("Starting account mqtt at 0.0.0.0:1883"); - let mut mqtt_options = MqttOptions::new(channels::account::CLIENT_NAME, "0.0.0.0", 1883); - mqtt_options.set_keep_alive(Duration::from_secs(5)); - - let (client, mut event_loop) = AsyncClient::new(mqtt_options, 10); - client - .subscribe(Topic::CreateAccount, QoS::AtLeastOnce) - .await - .unwrap(); - - let client = channels::AsyncClient(client); - let spawn_client = client.clone(); - tokio::spawn(async move { - let client = spawn_client.clone(); - loop { - let notification = event_loop.poll().await; - - match notification { - Ok(Event::Incoming(Incoming::Publish(publish))) => match publish.topic.as_str() { - topic if Topic::CreateAccount == topic => { - if let Ok(channels::account::CreateAccount { - email, - login, - password, - role, - }) = channels::account::CreateAccount::try_from(publish.payload) - { - match actions::create_account( - CreateAccount { - email, - login, - password, - role, - }, - &db, - config.clone(), - ) - .await - { - Ok(account) => { - client - .publish_or_log( - Topic::AccountCreated, - QoS::AtLeastOnce, - true, - model::Account::from(account), - ) - .await; - } - Err(e) => { - tracing::error!("{}", e); - let m = match e { - Error::Hashing => { - Some(AccountFailure::FailedToHashPassword) - } - Error::Saving => Some(AccountFailure::SaveAccount), - Error::DbCritical => { - Some(AccountFailure::InternalServerError) - } - _ => None, - }; - if let Some(m) = m { - client - .publish_or_log( - Topic::SignUpFailure, - QoS::AtLeastOnce, - true, - m, - ) - .await; - } - } - } - } - } - _ => {} - }, - Ok(Event::Incoming(_incoming)) => {} - Ok(Event::Outgoing(_outgoing)) => {} - Err(e) => { - tracing::error!("{}", e); - } - } - } - }); - - client - // tracing::info!("Mqtt channel closed"); -} diff --git a/actors/cart_manager/Cargo.toml b/actors/cart_manager/Cargo.toml deleted file mode 100644 index 640acb2..0000000 --- a/actors/cart_manager/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "cart_manager" -version = "0.1.0" -edition = "2021" - -[dependencies] -actix = { version = "0.13", features = [] } -actix-rt = { version = "2.7", features = [] } -channels = { path = "../../shared/channels" } -chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } -database_manager = { path = "../database_manager" } -model = { path = "../../shared/model" } -pretty_env_logger = { version = "0.4", features = [] } -rumqttc = { version = "*" } -serde = { version = "1.0.137", features = ["derive"] } -thiserror = { version = "1.0.31" } -tracing = { version = "0.1.34" } -uuid = { version = "0.8", features = ["serde"] } diff --git a/actors/cart_manager/src/lib.rs b/actors/cart_manager/src/lib.rs deleted file mode 100644 index 9409a39..0000000 --- a/actors/cart_manager/src/lib.rs +++ /dev/null @@ -1,286 +0,0 @@ -#![feature(drain_filter)] - -use std::collections::HashSet; - -use database_manager::{query_db, Database}; -use model::{PaymentMethod, ShoppingCartId}; - -#[macro_export] -macro_rules! cart_async_handler { - ($msg: ty, $async: ident, $res: ty) => { - impl actix::Handler<$msg> for CartManager { - type Result = actix::ResponseActFuture>; - - fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { - use actix::WrapFuture; - let db = self.db.clone(); - Box::pin(async { $async(msg, db).await }.into_actor(self)) - } - } - }; -} - -#[macro_export] -macro_rules! query_cart { - ($cart: expr, $msg: expr, default $fail: expr) => { - match $cart.send($msg).await { - Ok(Ok(r)) => r, - Ok(Err(e)) => { - tracing::error!("{e}"); - $fail - } - Err(e) => { - tracing::error!("{e:?}"); - $fail - } - } - }; - - ($cart: expr, $msg: expr, $fail: expr) => { - $crate::query_cart!($cart, $msg, $fail, $fail) - }; - - ($cart: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => { - match $cart.send($msg).await { - Ok(Ok(r)) => r, - Ok(Err(e)) => { - tracing::error!("{e}"); - return Err($db_fail); - } - Err(e) => { - tracing::error!("{e:?}"); - return Err($act_fail); - } - } - }; -} - -#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] -#[serde(rename_all = "kebab-case", tag = "cart")] -pub enum Error { - #[error("System can't ensure shopping cart existence")] - ShoppingCartFailed, - #[error("Shopping cart is not available for unknown reason")] - CartNotAvailable, - #[error("Failed to modify item to cart")] - CantModifyItem, - #[error("Failed to modify cart")] - CantModifyCart, - #[error("{0}")] - Db(#[from] database_manager::Error), - #[error("Unable to update cart item")] - UpdateFailed, -} - -pub type Result = std::result::Result; - -pub struct CartManager { - db: actix::Addr, -} - -impl actix::Actor for CartManager { - type Context = actix::Context; -} - -impl CartManager { - pub fn new(db: actix::Addr) -> Self { - Self { db } - } -} - -#[derive(actix::Message, Debug)] -#[rtype(result = "Result>")] -pub struct ModifyItem { - pub buyer_id: model::AccountId, - pub product_id: model::ProductId, - pub quantity: model::Quantity, - pub quantity_unit: model::QuantityUnit, -} - -cart_async_handler!(ModifyItem, modify_item, Option); - -async fn modify_item( - msg: ModifyItem, - db: actix::Addr, -) -> Result> { - let _cart = query_db!( - db, - database_manager::EnsureActiveShoppingCart { - buyer_id: msg.buyer_id, - }, - Error::ShoppingCartFailed - ); - let mut carts: Vec = query_db!( - db, - database_manager::AccountShoppingCarts { - account_id: msg.buyer_id, - state: Some(model::ShoppingCartState::Active), - }, - passthrough Error::Db, - Error::CartNotAvailable - ); - let cart = if carts.is_empty() { - return Err(Error::CartNotAvailable); - } else { - carts.remove(0) - }; - - let item: Option = query_db!( - db, - database_manager::ActiveCartItemByProduct { - product_id: msg.product_id - }, - Error::CantModifyItem - ); - - match item { - Some(item) if **item.quantity == 0 => Ok(query_db!( - db, - database_manager::DeleteShoppingCartItem { id: item.id }, - passthrough Error::Db, - Error::CantModifyItem - )), - Some(item) => Ok(Some(query_db!( - db, - database_manager::UpdateShoppingCartItem { - id: item.id, - product_id: msg.product_id, - shopping_cart_id: cart.id, - quantity: msg.quantity, - quantity_unit: msg.quantity_unit, - }, - passthrough Error::Db, - Error::CantModifyItem - ))), - None => Ok(Some(query_db!( - db, - database_manager::CreateShoppingCartItem { - product_id: msg.product_id, - shopping_cart_id: cart.id, - quantity: msg.quantity, - quantity_unit: msg.quantity_unit, - }, - passthrough Error::Db, - Error::CantModifyItem - ))), - } -} - -#[derive(actix::Message)] -#[rtype(result = "Result>")] -pub struct RemoveProduct { - pub shopping_cart_id: model::ShoppingCartId, - pub shopping_cart_item_id: model::ShoppingCartItemId, -} - -cart_async_handler!( - RemoveProduct, - remove_product, - Option -); - -pub(crate) async fn remove_product( - msg: RemoveProduct, - db: actix::Addr, -) -> Result> { - Ok(query_db!( - db, - database_manager::RemoveCartItem { - shopping_cart_id: msg.shopping_cart_id, - shopping_cart_item_id: Some(msg.shopping_cart_item_id), - product_id: None, - }, - Error::UpdateFailed - )) -} - -pub struct ModifyCartResult { - pub cart_id: ShoppingCartId, - pub items: Vec, - pub checkout_notes: String, - pub payment_method: model::PaymentMethod, -} - -#[derive(actix::Message, Debug)] -#[rtype(result = "Result")] -pub struct ModifyCart { - pub buyer_id: model::AccountId, - pub items: Vec, - pub checkout_notes: String, - pub payment_method: Option, -} - -cart_async_handler!(ModifyCart, modify_cart, ModifyCartResult); - -async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result { - tracing::debug!("{:?}", msg); - let cart: model::ShoppingCart = query_db!( - db, - database_manager::EnsureActiveShoppingCart { - buyer_id: msg.buyer_id, - }, - Error::ShoppingCartFailed - ); - let cart: model::ShoppingCart = query_db!( - db, - database_manager::UpdateShoppingCart { - id: cart.id, - buyer_id: msg.buyer_id, - payment_method: msg.payment_method.unwrap_or(cart.payment_method), - state: model::ShoppingCartState::Active, - checkout_notes: if msg.checkout_notes.is_empty() { - None - } else { - Some(msg.checkout_notes) - } - }, - passthrough Error::Db, - Error::CartNotAvailable - ); - - let existing = - msg.items - .iter() - .fold(HashSet::with_capacity(msg.items.len()), |mut agg, item| { - agg.insert(item.product_id); - agg - }); - - let items: Vec = query_db!( - db, - database_manager::CartItems { - shopping_cart_id: cart.id - }, - Error::CantModifyCart - ); - - for item in items - .into_iter() - .filter(|item| !existing.contains(&item.product_id)) - { - query_db!( - db, - database_manager::RemoveCartItem { - shopping_cart_id: cart.id, - shopping_cart_item_id: Some(item.id), - product_id: None, - }, - Error::CantModifyCart - ); - } - - let mut out = Vec::with_capacity(msg.items.len()); - - for item in msg.items { - if let Some(item) = modify_item(item, db.clone()).await? { - out.push(item); - } - } - - Ok(ModifyCartResult { - cart_id: cart.id, - items: out, - checkout_notes: cart.checkout_notes.unwrap_or_default(), - payment_method: cart.payment_method, - }) -} diff --git a/actors/account_manager/Cargo.toml b/crates/account_manager/Cargo.toml similarity index 78% rename from actors/account_manager/Cargo.toml rename to crates/account_manager/Cargo.toml index 4d3e8e4..10e1913 100644 --- a/actors/account_manager/Cargo.toml +++ b/crates/account_manager/Cargo.toml @@ -5,26 +5,27 @@ edition = "2021" [[bin]] name = "account-manager" -path = "./src/main.rs" +path = "src/main.rs" [dependencies] actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } bincode = { version = "1.3.3" } bytes = { version = "1.2.1" } -channels = { path = "../../shared/channels" } -config = { path = "../../shared/config" } -database_manager = { path = "../database_manager" } +channels = { path = "../channels" } +config = { path = "../config" } dotenv = { version = "0.15.0" } futures = { version = "0.3.25" } gumdrop = { version = "0.8.1" } json = { version = "0.12.4" } -model = { path = "../../shared/model" } +model = { path = "../model" } opentelemetry = { version = "0.17.0" } opentelemetry-jaeger = { version = "0.17.0" } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } serde = { version = "1.0.137", features = ["derive"] } +sqlx = { version = "0.6.2", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } +sqlx-core = { version = "0.6.2", features = [] } tarpc = { version = "0.30.0", features = ["tokio1", "serde-transport-bincode", "serde-transport", "serde", "serde-transport-json", "tcp"] } thiserror = { version = "1.0.31" } tokio = { version = "1.21.2", features = ['full'] } diff --git a/crates/account_manager/migrations/202204131841_init.sql b/crates/account_manager/migrations/202204131841_init.sql new file mode 100644 index 0000000..464e86e --- /dev/null +++ b/crates/account_manager/migrations/202204131841_init.sql @@ -0,0 +1,22 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; + +CREATE TYPE "AccountState" AS ENUM ( + 'active', + 'suspended', + 'banned' +); + +CREATE TYPE "Role" AS ENUM ( + 'admin', + 'user' +); + +CREATE TABLE public.accounts ( + id integer NOT NULL, + email character varying NOT NULL, + login character varying NOT NULL, + pass_hash character varying NOT NULL, + role "Role" DEFAULT 'user'::"Role" NOT NULL, + customer_id uuid DEFAULT gen_random_uuid() NOT NULL, + state "AccountState" DEFAULT 'active'::"AccountState" NOT NULL +); diff --git a/crates/account_manager/migrations/202204131842_addresses.sql b/crates/account_manager/migrations/202204131842_addresses.sql new file mode 100644 index 0000000..4a301f6 --- /dev/null +++ b/crates/account_manager/migrations/202204131842_addresses.sql @@ -0,0 +1,12 @@ +CREATE TABLE public.account_addresses ( + id integer NOT NULL, + name text NOT NULL, + email text NOT NULL, + street text NOT NULL, + city text NOT NULL, + country text NOT NULL, + zip text NOT NULL, + account_id integer, + is_default boolean DEFAULT false NOT NULL, + phone text DEFAULT ''::text NOT NULL +); diff --git a/actors/account_manager/src/actions.rs b/crates/account_manager/src/actions.rs similarity index 51% rename from actors/account_manager/src/actions.rs rename to crates/account_manager/src/actions.rs index 456dad0..3872584 100644 --- a/actors/account_manager/src/actions.rs +++ b/crates/account_manager/src/actions.rs @@ -1,37 +1,55 @@ -use channels::account::{CreateAccount, MeResult}; +use channels::accounts::{me, register}; use config::SharedAppConfig; -use database_manager::Database; use model::{Encrypt, FullAccount}; +use crate::db::{AccountAddresses, Database, FindAccount}; use crate::{Error, Result}; #[allow(unused)] -pub async fn me(account_id: model::AccountId, db: Database) -> MeResult { - use channels::account::Error; +pub async fn me(account_id: model::AccountId, db: Database) -> me::Output { + use channels::accounts::Error; - let msg = database_manager::FindAccount { account_id }; - let account: model::FullAccount = match msg.inner_find_account(db.pool().clone()).await { + let mut t = match db.pool.begin().await { + Ok(t) => t, + Err(e) => { + tracing::error!("{}", e); + return me::Output { + account: None, + addresses: None, + error: Some(Error::Account), + }; + } + }; + + let res = FindAccount { account_id }.run(&mut t).await; + let account: model::FullAccount = match res { Ok(account) => account, Err(e) => { tracing::error!("{}", e); - return MeResult { + t.rollback().await.ok(); + + return me::Output { error: Some(Error::Account), ..Default::default() }; } }; - let msg = database_manager::AccountAddresses { account_id }; - let addresses = match msg.inner_account_addresses(db.pool().clone()).await { + let res = AccountAddresses { account_id }.run(&mut t).await; + let addresses = match res { Ok(v) => v, Err(e) => { tracing::error!("{}", e); - return MeResult { + t.rollback().await.ok(); + + return me::Output { error: Some(Error::Addresses), ..Default::default() }; } }; - MeResult { + t.commit().await.ok(); + + me::Output { account: Some(account), addresses: Some(addresses), ..Default::default() @@ -39,7 +57,7 @@ pub async fn me(account_id: model::AccountId, db: Database) -> MeResult { } pub async fn create_account( - msg: CreateAccount, + msg: register::Input, db: &Database, config: SharedAppConfig, ) -> Result { @@ -51,21 +69,20 @@ pub async fn create_account( Error::Hashing })?; - let mut t = db.pool().begin().await.map_err(|e| { + let mut t = db.pool.begin().await.map_err(|e| { tracing::error!("{}", e); Error::DbCritical })?; - let account: FullAccount = match database_manager::create_account( - database_manager::CreateAccount { - email: msg.email, - login: msg.login, - pass_hash: model::PassHash::new(hash), - role: msg.role, - }, - &mut t, - ) - .await - { + let res = crate::db::CreateAccount { + email: msg.email, + login: msg.login, + pass_hash: model::PassHash::new(hash), + role: msg.role, + } + .run(&mut t) + .await; + + let account: FullAccount = match res { Ok(r) => r, Err(e) => { tracing::error!("{}", e); diff --git a/actors/account_manager/src/bin/account-client.rs b/crates/account_manager/src/bin/account-client.rs similarity index 93% rename from actors/account_manager/src/bin/account-client.rs rename to crates/account_manager/src/bin/account-client.rs index 682cf4d..58ff77f 100644 --- a/actors/account_manager/src/bin/account-client.rs +++ b/crates/account_manager/src/bin/account-client.rs @@ -20,7 +20,7 @@ async fn main() -> std::io::Result<()> { let opts: Flags = gumdrop::Options::parse_args_default_or_exit(); let config = config::default_load(&opts); - let client = channels::account::rpc::create_client(config).await; + let client = channels::accounts::rpc::create_client(config).await; let r = client.me(context::current(), 1.into()).await; println!("{:?}", r); diff --git a/crates/account_manager/src/db/accounts.rs b/crates/account_manager/src/db/accounts.rs new file mode 100644 index 0000000..cf004f8 --- /dev/null +++ b/crates/account_manager/src/db/accounts.rs @@ -0,0 +1,428 @@ +use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role}; + +pub type Result = std::result::Result; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +pub enum Error { + #[error("Can't create account")] + CantCreate, + #[error("Can't find account does to lack of identity")] + NoIdentity, + #[error("Account does not exists")] + NotExists, + #[error("Failed to load all accounts")] + All, + #[error("Can't update account")] + CantUpdate, +} + +#[derive(Debug)] +pub struct AllAccounts; + +impl AllAccounts { + pub async fn run( + _msg: AllAccounts, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + sqlx::query_as( + r#" +SELECT id, email, login, pass_hash, role, customer_id, state +FROM accounts + "#, + ) + .fetch_all(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::All + }) + } +} + +#[derive(Debug)] +pub struct CreateAccount { + pub email: Email, + pub login: Login, + pub pass_hash: PassHash, + pub role: Role, +} + +impl CreateAccount { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + sqlx::query_as( + r#" +INSERT INTO accounts (login, email, role, pass_hash) +VALUES ($1, $2, $3, $4) +RETURNING id, email, login, pass_hash, role, customer_id, state + "#, + ) + .bind(self.login) + .bind(self.email) + .bind(self.role) + .bind(self.pass_hash) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantCreate + }) + } +} + +#[derive(Debug)] +pub struct UpdateAccount { + pub id: AccountId, + pub email: Email, + pub login: Login, + pub pass_hash: Option, + pub role: Role, + pub state: AccountState, +} + +impl UpdateAccount { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + match self.pass_hash { + Some(hash) => sqlx::query_as( + r#" +UPDATE accounts +SET login = $2, email = $3, role = $4, pass_hash = $5, state = $6 +WHERE id = $1 +RETURNING id, email, login, pass_hash, role, customer_id, state + "#, + ) + .bind(self.id) + .bind(self.login) + .bind(self.email) + .bind(self.role) + .bind(hash) + .bind(self.state), + None => sqlx::query_as( + r#" +UPDATE accounts +SET login = $2, email = $3, role = $4, state = $5 +WHERE id = $1 +RETURNING id, email, login, pass_hash, role, customer_id, state + "#, + ) + .bind(self.id) + .bind(self.login) + .bind(self.email) + .bind(self.role) + .bind(self.state), + } + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantUpdate + }) + } +} + +#[derive(Debug)] +pub struct FindAccount { + pub account_id: AccountId, +} + +impl FindAccount { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + sqlx::query_as( + r#" +SELECT id, email, login, pass_hash, role, customer_id, state +FROM accounts +WHERE id = $1 + "#, + ) + .bind(self.account_id) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::NotExists + }) + } +} + +#[derive(Debug)] +pub struct AccountByIdentity { + pub login: Option, + pub email: Option, +} + +impl AccountByIdentity { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + match (self.login, self.email) { + (Some(login), None) => sqlx::query_as( + r#" +SELECT id, email, login, pass_hash, role, customer_id, state +FROM accounts +WHERE login = $1 + "#, + ) + .bind(login), + (None, Some(email)) => sqlx::query_as( + r#" +SELECT id, email, login, pass_hash, role, customer_id, state +FROM accounts +WHERE email = $1 + "#, + ) + .bind(email), + (Some(login), Some(email)) => sqlx::query_as( + r#" +SELECT id, email, login, pass_hash, role, customer_id, state +FROM accounts +WHERE login = $1 AND email = $2 + "#, + ) + .bind(login) + .bind(email), + _ => return Err(Error::NoIdentity), + } + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantCreate + }) + } +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use fake::Fake; + use model::*; + + use super::*; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + async fn test_create_account( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + login: Option, + email: Option, + hash: Option, + ) -> FullAccount { + use fake::faker::internet::en; + let login: String = login.unwrap_or_else(|| en::Username().fake()); + let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); + let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); + + CreateAccount { + email: Email::new(email), + login: Login::new(login), + pass_hash: PassHash::new(hash), + role: Role::Admin, + } + .run(t) + .await + .unwrap() + } + + #[actix::test] + async fn create_account() { + testx::db_t_ref!(t); + + let login: String = fake::faker::internet::en::Username().fake(); + let email: String = fake::faker::internet::en::FreeEmail().fake(); + let hash: String = fake::faker::internet::en::Password(10..20).fake(); + + let account: FullAccount = CreateAccount { + email: Email::new(&email), + login: Login::new(&login), + pass_hash: PassHash::new(&hash), + role: Role::Admin, + } + .run(&mut t) + .await + .unwrap(); + + let expected = FullAccount { + login: Login::new(login), + email: Email::new(email), + pass_hash: PassHash::new(&hash), + role: Role::Admin, + customer_id: account.customer_id, + id: account.id, + state: AccountState::Active, + }; + + t.rollback().await.unwrap(); + assert_eq!(account, expected); + } + + #[actix::test] + async fn all_accounts() { + testx::db_t_ref!(t); + + test_create_account(&mut t, None, None, None).await; + test_create_account(&mut t, None, None, None).await; + test_create_account(&mut t, None, None, None).await; + + let v: Vec = AllAccounts.run(&mut t).await.unwrap(); + + testx::db_rollback!(t); + assert!(v.len() >= 3); + } + + #[actix::test] + async fn update_account_without_pass() { + testx::db_t_ref!(t); + + let original_login: String = fake::faker::internet::en::Username().fake(); + let original_email: String = fake::faker::internet::en::FreeEmail().fake(); + let original_hash: String = fake::faker::internet::en::Password(10..20).fake(); + + let original_account = test_create_account( + &mut t, + Some(original_login.clone()), + Some(original_email.clone()), + Some(original_hash.clone()), + ) + .await; + + let updated_login: String = fake::faker::internet::en::Username().fake(); + let updated_email: String = fake::faker::internet::en::FreeEmail().fake(); + + let updated_account: FullAccount = UpdateAccount { + id: original_account.id, + email: Email::new(updated_email.clone()), + login: Login::new(updated_login.clone()), + pass_hash: None, + role: Role::Admin, + state: AccountState::Active, + } + .run(&mut t) + .await + .unwrap(); + + let expected = FullAccount { + id: original_account.id, + email: Email::new(updated_email), + login: Login::new(updated_login), + pass_hash: PassHash::new(original_hash), + role: Role::Admin, + customer_id: original_account.customer_id, + state: AccountState::Active, + }; + + testx::db_rollback!(t); + assert_ne!(original_account, expected); + assert_eq!(updated_account, expected); + } + + #[actix::test] + async fn update_account_with_pass() { + testx::db_t_ref!(t); + + let original_login: String = fake::faker::internet::en::Username().fake(); + let original_email: String = fake::faker::internet::en::FreeEmail().fake(); + let original_hash: String = fake::faker::internet::en::Password(10..20).fake(); + + let original_account = test_create_account( + &mut t, + Some(original_login.clone()), + Some(original_email.clone()), + Some(original_hash.clone()), + ) + .await; + + let updated_login: String = fake::faker::internet::en::Username().fake(); + let updated_email: String = fake::faker::internet::en::FreeEmail().fake(); + let updated_hash: String = fake::faker::internet::en::Password(10..20).fake(); + + let updated_account: FullAccount = UpdateAccount { + id: original_account.id, + email: Email::new(updated_email.clone()), + login: Login::new(updated_login.clone()), + pass_hash: Some(PassHash::new(updated_hash.clone())), + role: Role::Admin, + state: AccountState::Active, + } + .run(&mut t) + .await + .unwrap(); + + let expected = FullAccount { + id: original_account.id, + email: Email::new(updated_email), + login: Login::new(updated_login), + pass_hash: PassHash::new(updated_hash), + role: Role::Admin, + customer_id: original_account.customer_id, + state: AccountState::Active, + }; + + testx::db_rollback!(t); + assert_ne!(original_account, expected); + assert_eq!(updated_account, expected); + } + + #[actix::test] + async fn find() { + testx::db_t_ref!(t); + + let account = test_create_account(&mut t, None, None, None).await; + + let res: FullAccount = FindAccount { + account_id: account.id, + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(account, res); + } + + #[actix::test] + async fn find_identity_email() { + testx::db_t_ref!(t); + + let account = test_create_account(&mut t, None, None, None).await; + + let res: FullAccount = AccountByIdentity { + email: Some(account.email.clone()), + login: None, + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(account, res); + } + + #[actix::test] + async fn find_identity_login() { + testx::db_t_ref!(t); + + let account = test_create_account(&mut t, None, None, None).await; + + let res: FullAccount = AccountByIdentity { + login: Some(account.login.clone()), + email: None, + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(account, res); + } +} diff --git a/crates/account_manager/src/db/addresses.rs b/crates/account_manager/src/db/addresses.rs new file mode 100644 index 0000000..69d4acf --- /dev/null +++ b/crates/account_manager/src/db/addresses.rs @@ -0,0 +1,312 @@ +pub type Result = std::result::Result; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Can't load account addresses")] + AccountAddresses, + #[error("Failed to save account address")] + CreateAccountAddress, +} + +#[derive(Debug)] +pub struct AccountAddresses { + pub account_id: model::AccountId, +} + +impl AccountAddresses { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + sqlx::query_as( + r#" +SELECT id, name, email, phone, street, city, country, zip, account_id, is_default +FROM account_addresses +WHERE account_id = $1 + "#, + ) + .bind(self.account_id) + .fetch_all(pool) + .await + .map_err(|_| Error::AccountAddresses.into()) + } +} + +#[derive(Debug)] +pub struct FindAccountAddress { + pub account_id: model::AccountId, + pub address_id: model::AddressId, +} + +impl FindAccountAddress { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + sqlx::query_as( + r#" +SELECT id, name, email, phone, street, city, country, zip, account_id, is_default +FROM account_addresses +WHERE account_id = $1 AND id = $2 + "#, + ) + .bind(self.account_id) + .bind(self.address_id) + .fetch_one(pool) + .await + .map_err(|_| Error::AccountAddresses.into()) + } +} + +#[derive(Debug)] +pub struct DefaultAccountAddress { + pub account_id: model::AccountId, +} + +impl DefaultAccountAddress { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + sqlx::query_as( + r#" +SELECT id, name, email, phone, street, city, country, zip, account_id, is_default +FROM account_addresses +WHERE account_id = $1 AND is_default + "#, + ) + .bind(self.account_id) + .fetch_one(pool) + .await + .map_err(|_| Error::AccountAddresses.into()) + } +} + +#[derive(Debug)] +pub struct CreateAccountAddress { + pub name: model::Name, + pub email: model::Email, + pub phone: model::Phone, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, + pub account_id: Option, + pub is_default: bool, +} + +impl CreateAccountAddress { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + if self.is_default && self.account_id.is_some() { + if let Err(e) = sqlx::query( + r#" +UPDATE account_addresses +SET is_default = FALSE +WHERE account_id = $1 + "#, + ) + .bind(self.account_id) + .fetch_all(&mut *pool) + .await + { + tracing::error!("{e}"); + dbg!(e); + } + } + + sqlx::query_as( + r#" +INSERT INTO account_addresses ( name, email, phone, street, city, country, zip, account_id, is_default) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +RETURNING id, name, email, phone, street, city, country, zip, account_id, is_default + "#, + ) + .bind(self.name) + .bind(self.email) + .bind(self.phone) + .bind(self.street) + .bind(self.city) + .bind(self.country) + .bind(self.zip) + .bind(self.account_id) + .bind(self.is_default) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e}"); + dbg!(e); + Error::CreateAccountAddress.into() + }) + } +} + +#[derive(Debug)] +pub struct UpdateAccountAddress { + pub id: model::AddressId, + pub name: model::Name, + pub email: model::Email, + pub phone: model::Phone, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, + pub account_id: model::AccountId, + pub is_default: bool, +} + +impl UpdateAccountAddress { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + sqlx::query_as( + r#" +UPDATE account_addresses +SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7, account_id = $8, is_default = $9, phone = $10 +WHERE id = $1 +RETURNING id, name, email, phone, street, city, country, zip, account_id, is_default + "#, + ) + .bind(self.id) + .bind(self.name) + .bind(self.email) + .bind(self.street) + .bind(self.city) + .bind(self.country) + .bind(self.zip) + .bind(self.account_id) + .bind(self.is_default) + .bind(self.phone) + .fetch_one(pool) + .await + .map_err(|_| Error::CreateAccountAddress.into()) + } +} + +#[cfg(test)] +mod test { + use config::*; + use fake::Fake; + use model::*; + + use super::super::accounts::CreateAccount; + use super::*; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + async fn test_create_account(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> FullAccount { + let login: String = fake::faker::internet::en::Username().fake(); + let email: String = fake::faker::internet::en::FreeEmail().fake(); + let hash: String = fake::faker::internet::en::Password(10..20).fake(); + + CreateAccount { + email: Email::new(email), + login: Login::new(login), + pass_hash: PassHash::new(hash), + role: Role::Admin, + } + .run(pool) + .await + .unwrap() + } + + #[actix::test] + async fn full_check() { + testx::db_t_ref!(t); + + // account + let account = test_create_account(&mut t).await; + + // address + let mut address: AccountAddress = { + let name: String = fake::faker::name::en::Name().fake(); + let email: String = fake::faker::internet::en::FreeEmail().fake(); + let phone: String = fake::faker::phone_number::en::PhoneNumber().fake(); + let street: String = fake::faker::address::en::StreetName().fake(); + let city: String = fake::faker::address::en::CityName().fake(); + let country: String = fake::faker::address::en::CountryName().fake(); + let zip: String = fake::faker::address::en::ZipCode().fake(); + let account_id = Some(account.id); + let is_default: bool = true; + + let address = CreateAccountAddress { + name: Name::new(name.clone()), + email: Email::new(email.clone()), + phone: Phone::new(phone.clone()), + street: Street::new(street.clone()), + city: City::new(city.clone()), + country: Country::new(country.clone()), + zip: Zip::new(zip.clone()), + account_id, + is_default, + } + .run(&mut t) + .await + .unwrap(); + + assert_eq!( + address, + AccountAddress { + id: address.id, + name: Name::new(name.clone()), + email: Email::new(email.clone()), + phone: Phone::new(phone.clone()), + street: Street::new(street.clone()), + city: City::new(city.clone()), + country: Country::new(country.clone()), + zip: Zip::new(zip.clone()), + account_id: account.id, + is_default, + } + ); + address + }; + + let found = super::find_account_address( + FindAccountAddress { + account_id: account.id, + address_id: address.id, + }, + &mut t, + ) + .await + .unwrap(); + + assert_eq!(found, address); + + let changed = UpdateAccountAddress { + id: address.id, + name: address.name.clone(), + email: address.email.clone(), + phone: address.phone.clone(), + street: address.street.clone(), + city: address.city.clone(), + country: address.country.clone(), + zip: address.zip.clone(), + account_id: address.account_id, + is_default: true, + } + .run(&mut t) + .await + .unwrap(); + + address.is_default = true; + + assert_eq!(changed, address); + + let default_address = DefaultAccountAddress { + account_id: account.id, + } + .run(&mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(default_address, address); + } +} diff --git a/crates/account_manager/src/db/mod.rs b/crates/account_manager/src/db/mod.rs new file mode 100644 index 0000000..bed853b --- /dev/null +++ b/crates/account_manager/src/db/mod.rs @@ -0,0 +1,28 @@ +pub mod accounts; +pub mod addresses; + +pub use accounts::*; +pub use addresses::*; +use config::SharedAppConfig; + +#[derive(Clone)] +pub struct Database { + pub pool: sqlx::PgPool, + _config: SharedAppConfig, +} + +impl Database { + pub async fn build(config: SharedAppConfig) -> Self { + let url = config.lock().account_manager().database_url.clone(); + let pool = sqlx::PgPool::connect(&url).await.unwrap_or_else(|e| { + tracing::error!("Failed to connect to database. {e:?}"); + std::process::exit(1); + }); + Self { + pool, + _config: config, + } + } + + pub fn pool(&self) {} +} diff --git a/actors/account_manager/src/main.rs b/crates/account_manager/src/main.rs similarity index 92% rename from actors/account_manager/src/main.rs rename to crates/account_manager/src/main.rs index e7484b0..60aac30 100644 --- a/actors/account_manager/src/main.rs +++ b/crates/account_manager/src/main.rs @@ -3,12 +3,12 @@ use std::env; use config::UpdateConfig; -use database_manager::Database; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; pub mod actions; +pub mod db; pub mod mqtt; pub mod rpc; @@ -26,8 +26,6 @@ pub enum Error { Saving, #[error("Unable to hash password")] Hashing, - #[error("{0}")] - Db(#[from] database_manager::Error), } pub struct Opts {} @@ -43,7 +41,7 @@ async fn main() { let config = config::default_load(&opts); - let db = Database::build(config.clone()).await; + let db = db::Database::build(config.clone()).await; let mqtt_client = mqtt::start(config.clone(), db.clone()).await; rpc::start(config.clone(), db.clone(), mqtt_client.clone()).await; @@ -55,6 +53,7 @@ pub fn init_tracing(_service_name: &str) { let tracer = { use opentelemetry::sdk::export::trace::stdout::new_pipeline; use opentelemetry::sdk::trace::Config; + new_pipeline() .with_trace_config(Config::default()) .with_pretty_print(true) diff --git a/crates/account_manager/src/mqtt.rs b/crates/account_manager/src/mqtt.rs new file mode 100644 index 0000000..079e484 --- /dev/null +++ b/crates/account_manager/src/mqtt.rs @@ -0,0 +1,48 @@ +use std::time::Duration; + +use channels::accounts::Topic; +use config::SharedAppConfig; +use rumqttc::{Event, Incoming, QoS}; + +use crate::db::Database; + +pub async fn start(config: SharedAppConfig, _db: Database) -> channels::AsyncClient { + let mut mqtt_options = { + let l = config.lock(); + let bind = &l.account_manager().mqtt_bind; + let port = l.account_manager().mqtt_port; + tracing::info!("Starting account mqtt at {}:{}", bind, port); + + rumqttc::MqttOptions::new(channels::accounts::CLIENT_NAME, bind, port) + }; + mqtt_options.set_keep_alive(Duration::from_secs(5)); + + let (client, mut event_loop) = rumqttc::AsyncClient::new(mqtt_options, 10); + client + .subscribe(Topic::CreateAccount, QoS::AtLeastOnce) + .await + .unwrap(); + + let client = channels::AsyncClient(client); + let spawn_client = client.clone(); + tokio::spawn(async move { + let _client = spawn_client.clone(); + loop { + let notification = event_loop.poll().await; + + match notification { + Ok(Event::Incoming(Incoming::Publish(publish))) => match publish.topic.as_str() { + _ => {} + }, + Ok(Event::Incoming(_incoming)) => {} + Ok(Event::Outgoing(_outgoing)) => {} + Err(e) => { + tracing::error!("{}", e); + } + } + } + // tracing::info!("Mqtt channel closed"); + }); + + client +} diff --git a/actors/account_manager/src/rpc.rs b/crates/account_manager/src/rpc.rs similarity index 71% rename from actors/account_manager/src/rpc.rs rename to crates/account_manager/src/rpc.rs index af430d1..869c2a8 100644 --- a/actors/account_manager/src/rpc.rs +++ b/crates/account_manager/src/rpc.rs @@ -1,18 +1,19 @@ use std::net::{IpAddr, Ipv4Addr}; -use channels::account::{CreateAccount, MeResult, RegisterResult}; +use channels::accounts::rpc::Accounts; +use channels::accounts::{me, register}; use channels::AsyncClient; use config::SharedAppConfig; -use database_manager::Database; use futures::future::{self}; use futures::stream::StreamExt; use rumqttc::QoS; use tarpc::context; use tarpc::server::incoming::Incoming; use tarpc::server::{self, Channel}; -use tarpc::tokio_serde::formats::Json; +use tarpc::tokio_serde::formats::Bincode; use crate::actions; +use crate::db::Database; #[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] #[serde(rename_all = "kebab-case", tag = "account")] @@ -27,8 +28,6 @@ pub enum Error { Saving, #[error("Unable to hash password")] Hashing, - #[error("{0}")] - Db(#[from] database_manager::Error), } #[derive(Clone)] @@ -39,47 +38,46 @@ struct AccountsServer { } #[tarpc::server] -impl channels::account::rpc::Accounts for AccountsServer { - async fn me(self, _: context::Context, account_id: model::AccountId) -> MeResult { - let res = actions::me(account_id, self.db).await; +impl Accounts for AccountsServer { + async fn me(self, _: context::Context, input: me::Input) -> me::Output { + let res = actions::me(input.account_id, self.db).await; tracing::info!("ME result: {:?}", res); res } - async fn register_account(self, _: context::Context, details: CreateAccount) -> RegisterResult { - let res = actions::create_account(details, &self.db, self.config).await; + async fn register_account( + self, + _: context::Context, + input: register::Input, + ) -> register::Output { + use channels::accounts::{Error, Topic}; + + let res = actions::create_account(input, &self.db, self.config).await; tracing::info!("REGISTER result: {:?}", res); match res { Ok(account) => { self.mqtt_client - .publish_or_log( - channels::account::Topic::AccountCreated, - QoS::AtLeastOnce, - true, - &account, - ) + .publish_or_log(Topic::AccountCreated, QoS::AtLeastOnce, true, &account) .await; - RegisterResult { + register::Output { account: Some(account), error: None, } } - Err(_e) => RegisterResult { + Err(_e) => register::Output { account: None, - error: Some(channels::account::Error::Account), + error: Some(Error::Account), }, } } } pub async fn start(config: SharedAppConfig, db: Database, mqtt_client: AsyncClient) { - use channels::account::rpc::Accounts; - let port = { config.lock().account_manager().port }; let server_addr = (IpAddr::V4(Ipv4Addr::LOCALHOST), port); - let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Json::default) + let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Bincode::default) .await .unwrap(); tracing::info!("Starting account rpc at {}", listener.local_addr()); diff --git a/api/Cargo.lock b/crates/api/Cargo.lock similarity index 100% rename from api/Cargo.lock rename to crates/api/Cargo.lock diff --git a/api/Cargo.toml b/crates/api/Cargo.toml similarity index 78% rename from api/Cargo.toml rename to crates/api/Cargo.toml index df2b82f..7bbe3fc 100644 --- a/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -18,40 +18,40 @@ actix-web-httpauth = { version = "0.6", features = [] } actix-web-opentelemetry = { version = "0.12", features = [] } async-trait = { version = "0.1", features = [] } bytes = { version = "1.1.0" } -cart_manager = { path = "../actors/cart_manager" } -channels = { path = "../shared/channels" } +cart_manager = { path = "../cart_manager" } +channels = { path = "../channels" } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../shared/config" } -database_manager = { path = "../actors/database_manager" } +config = { path = "../config" } +database_manager = { path = "../database_manager" } derive_more = { version = "0.99", features = [] } dotenv = { version = "0.15", features = [] } -email_manager = { path = "../actors/email_manager" } -fs_manager = { path = "../actors/fs_manager" } +email_manager = { path = "../email_manager" } +fs_manager = { path = "../fs_manager" } futures = { version = "0.3", features = [] } futures-util = { version = "0.3", features = [] } gumdrop = { version = "0.8", features = [] } human-panic = { version = "1.0.3" } include_dir = { version = "0.7.2", features = [] } jemallocator = { version = "0.3", features = [] } -model = { path = "../shared/model", version = "0.1", features = ["db"] } +model = { path = "../model", version = "0.1", features = ["db"] } oauth2 = { version = "4.1", features = [] } -order_manager = { path = "../actors/order_manager" } +order_manager = { path = "../order_manager" } parking_lot = { version = "0.12", features = [] } -payment_manager = { path = "../actors/payment_manager" } +payment_manager = { path = "../payment_manager" } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } -search_manager = { path = "../actors/search_manager" } +search_manager = { path = "../search_manager" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = [] } sqlx = { version = "0.6.2", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } sqlx-core = { version = "0.6.2", features = [] } +tarpc = { version = "0.30.0", features = ["tokio1", "serde-transport-bincode", "serde-transport", "serde", "serde-transport-json", "tcp"] } tera = { version = "1.15", features = [] } thiserror = { version = "1.0", features = [] } -token_manager = { path = "../actors/token_manager" } +token_manager = { path = "../token_manager" } tokio = { version = "1.17", features = ["full"] } toml = { version = "0.5", features = [] } tracing = { version = "0.1.34" } tracing-subscriber = { version = "0.3.11" } uuid = { version = "1.2.1", features = ["serde"] } validator = { version = "0.14", features = [] } -tarpc = { version = "0.30.0", features = ["tokio1", "serde-transport-bincode", "serde-transport", "serde", "serde-transport-json", "tcp"] } diff --git a/api/assets/index.html b/crates/api/assets/index.html similarity index 100% rename from api/assets/index.html rename to crates/api/assets/index.html diff --git a/api/assets/svg/cameras.svg b/crates/api/assets/svg/cameras.svg similarity index 100% rename from api/assets/svg/cameras.svg rename to crates/api/assets/svg/cameras.svg diff --git a/api/assets/svg/clothes.svg b/crates/api/assets/svg/clothes.svg similarity index 100% rename from api/assets/svg/clothes.svg rename to crates/api/assets/svg/clothes.svg diff --git a/api/assets/svg/drugstore.svg b/crates/api/assets/svg/drugstore.svg similarity index 100% rename from api/assets/svg/drugstore.svg rename to crates/api/assets/svg/drugstore.svg diff --git a/api/assets/svg/fruits.svg b/crates/api/assets/svg/fruits.svg similarity index 100% rename from api/assets/svg/fruits.svg rename to crates/api/assets/svg/fruits.svg diff --git a/api/assets/svg/memory.svg b/crates/api/assets/svg/memory.svg similarity index 100% rename from api/assets/svg/memory.svg rename to crates/api/assets/svg/memory.svg diff --git a/api/assets/svg/pants.svg b/crates/api/assets/svg/pants.svg similarity index 100% rename from api/assets/svg/pants.svg rename to crates/api/assets/svg/pants.svg diff --git a/api/assets/svg/phones.svg b/crates/api/assets/svg/phones.svg similarity index 100% rename from api/assets/svg/phones.svg rename to crates/api/assets/svg/phones.svg diff --git a/api/assets/svg/plate.svg b/crates/api/assets/svg/plate.svg similarity index 100% rename from api/assets/svg/plate.svg rename to crates/api/assets/svg/plate.svg diff --git a/api/assets/svg/speakers.svg b/crates/api/assets/svg/speakers.svg similarity index 100% rename from api/assets/svg/speakers.svg rename to crates/api/assets/svg/speakers.svg diff --git a/api/assets/svg/sweets.svg b/crates/api/assets/svg/sweets.svg similarity index 100% rename from api/assets/svg/sweets.svg rename to crates/api/assets/svg/sweets.svg diff --git a/api/assets/svg/vegetables.svg b/crates/api/assets/svg/vegetables.svg similarity index 100% rename from api/assets/svg/vegetables.svg rename to crates/api/assets/svg/vegetables.svg diff --git a/api/i18n.toml b/crates/api/i18n.toml similarity index 100% rename from api/i18n.toml rename to crates/api/i18n.toml diff --git a/api/locales/en.yml b/crates/api/locales/en.yml similarity index 100% rename from api/locales/en.yml rename to crates/api/locales/en.yml diff --git a/api/src/main.rs b/crates/api/src/main.rs similarity index 100% rename from api/src/main.rs rename to crates/api/src/main.rs diff --git a/api/src/opts.rs b/crates/api/src/opts.rs similarity index 100% rename from api/src/opts.rs rename to crates/api/src/opts.rs diff --git a/api/src/routes/admin/api_v1/accounts.rs b/crates/api/src/routes/admin/api_v1/accounts.rs similarity index 100% rename from api/src/routes/admin/api_v1/accounts.rs rename to crates/api/src/routes/admin/api_v1/accounts.rs diff --git a/api/src/routes/admin/api_v1/mod.rs b/crates/api/src/routes/admin/api_v1/mod.rs similarity index 100% rename from api/src/routes/admin/api_v1/mod.rs rename to crates/api/src/routes/admin/api_v1/mod.rs diff --git a/api/src/routes/admin/api_v1/orders.rs b/crates/api/src/routes/admin/api_v1/orders.rs similarity index 100% rename from api/src/routes/admin/api_v1/orders.rs rename to crates/api/src/routes/admin/api_v1/orders.rs diff --git a/api/src/routes/admin/api_v1/products.rs b/crates/api/src/routes/admin/api_v1/products.rs similarity index 100% rename from api/src/routes/admin/api_v1/products.rs rename to crates/api/src/routes/admin/api_v1/products.rs diff --git a/api/src/routes/admin/api_v1/stocks.rs b/crates/api/src/routes/admin/api_v1/stocks.rs similarity index 100% rename from api/src/routes/admin/api_v1/stocks.rs rename to crates/api/src/routes/admin/api_v1/stocks.rs diff --git a/api/src/routes/admin/api_v1/uploads.rs b/crates/api/src/routes/admin/api_v1/uploads.rs similarity index 100% rename from api/src/routes/admin/api_v1/uploads.rs rename to crates/api/src/routes/admin/api_v1/uploads.rs diff --git a/api/src/routes/admin/mod.rs b/crates/api/src/routes/admin/mod.rs similarity index 100% rename from api/src/routes/admin/mod.rs rename to crates/api/src/routes/admin/mod.rs diff --git a/api/src/routes/mod.rs b/crates/api/src/routes/mod.rs similarity index 100% rename from api/src/routes/mod.rs rename to crates/api/src/routes/mod.rs diff --git a/api/src/routes/public/api_v1/mod.rs b/crates/api/src/routes/public/api_v1/mod.rs similarity index 100% rename from api/src/routes/public/api_v1/mod.rs rename to crates/api/src/routes/public/api_v1/mod.rs diff --git a/api/src/routes/public/api_v1/restricted.rs b/crates/api/src/routes/public/api_v1/restricted.rs similarity index 100% rename from api/src/routes/public/api_v1/restricted.rs rename to crates/api/src/routes/public/api_v1/restricted.rs diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/crates/api/src/routes/public/api_v1/unrestricted.rs similarity index 100% rename from api/src/routes/public/api_v1/unrestricted.rs rename to crates/api/src/routes/public/api_v1/unrestricted.rs diff --git a/api/src/routes/public/mod.rs b/crates/api/src/routes/public/mod.rs similarity index 100% rename from api/src/routes/public/mod.rs rename to crates/api/src/routes/public/mod.rs diff --git a/crates/cart_manager/Cargo.toml b/crates/cart_manager/Cargo.toml new file mode 100644 index 0000000..109a8ee --- /dev/null +++ b/crates/cart_manager/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "cart_manager" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "cart-manager" +path = "src/main.rs" + +[dependencies] +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } +channels = { path = "../channels" } +chrono = { version = "0.4", features = ["serde"] } +config = { path = "../config" } +dotenv = { version = "0.15.0" } +futures = { version = "0.3.25" } +model = { path = "../model" } +opentelemetry = { version = "0.17.0" } +opentelemetry-jaeger = { version = "0.17.0" } +pretty_env_logger = { version = "0.4", features = [] } +rumqttc = { version = "*" } +serde = { version = "1.0.137", features = ["derive"] } +sqlx = { version = "0.6.2", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] } +sqlx-core = { version = "0.6.2", features = [] } +tarpc = { version = "0.30.0", features = ["tokio1", "serde-transport-bincode", "serde-transport", "serde", "serde-transport-json", "tcp"] } +thiserror = { version = "1.0.31" } +tokio = { version = "1.21.2", features = ['full'] } +tracing = { version = "0.1.37" } +tracing-opentelemetry = { version = "0.17.4" } +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +uuid = { version = "0.8", features = ["serde"] } diff --git a/crates/cart_manager/migrations/202204131841_init.sql b/crates/cart_manager/migrations/202204131841_init.sql new file mode 100644 index 0000000..78eaa90 --- /dev/null +++ b/crates/cart_manager/migrations/202204131841_init.sql @@ -0,0 +1,33 @@ +CREATE TYPE "PaymentMethod" AS ENUM ( + 'pay_u', + 'payment_on_the_spot' +); + +CREATE TYPE "ShoppingCartState" AS ENUM ( + 'active', + 'closed' +); + +CREATE TYPE "QuantityUnit" AS ENUM ( + 'g', + 'dkg', + 'kg', + 'piece' +); + +CREATE TABLE shopping_carts ( + id integer NOT NULL, + buyer_id integer NOT NULL, + payment_method "PaymentMethod" DEFAULT 'payment_on_the_spot'::"PaymentMethod" NOT NULL, + state "ShoppingCartState" DEFAULT 'active'::"ShoppingCartState" NOT NULL, + checkout_notes text +); + +CREATE TABLE shopping_cart_items ( + id integer NOT NULL, + product_id integer NOT NULL, + shopping_cart_id integer, + quantity integer DEFAULT 0 NOT NULL, + quantity_unit "QuantityUnit" NOT NULL, + CONSTRAINT positive_quantity CHECK ((quantity >= 0)) +); diff --git a/crates/cart_manager/src/actions.rs b/crates/cart_manager/src/actions.rs new file mode 100644 index 0000000..c073e7b --- /dev/null +++ b/crates/cart_manager/src/actions.rs @@ -0,0 +1,250 @@ +use std::collections::HashSet; + +use channels::carts::modify_cart::CartDetails; +use channels::carts::{self, Error}; + +use crate::db::*; + +macro_rules! begin_t { + ($db: ident) => { + match $db.pool.begin().await { + Ok(t) => t, + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::InternalServerError); + } + } + }; +} + +macro_rules! end_t { + ($t: ident, $res: expr) => { + if let Err(e) = $t.commit().await { + tracing::error!("{}", e); + Output::error(Error::InternalServerError) + } else { + $res + } + }; +} + +pub async fn modify_item( + msg: carts::modify_item::Input, + db: Database, +) -> carts::modify_item::Output { + use channels::carts::modify_item::Output; + + let mut t = begin_t!(db); + + let dbm = EnsureActiveShoppingCart { + buyer_id: msg.buyer_id, + }; + if let Err(e) = dbm.run(&mut t).await { + tracing::error!("{}", e); + t.rollback().await.ok(); + return Output::error(Error::InternalServerError); + } + + let dbm = AccountShoppingCarts { + account_id: msg.buyer_id, + state: Some(model::ShoppingCartState::Active), + }; + let mut carts: Vec = match dbm.run(db.pool()).await { + Ok(carts) => carts, + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::NoCarts); + } + }; + let cart = if carts.is_empty() { + return Output::error(Error::NoCarts); + } else { + carts.remove(0) + }; + + let dbm = ActiveCartItemByProduct { + product_id: msg.product_id, + }; + let item: Option = match dbm.run(&mut t).await { + Ok(res) => res, + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::NoActiveCart); + } + }; + + let res = match item { + Some(item) if **item.quantity == 0 => { + let dbm = DeleteShoppingCartItem { id: item.id }; + match dbm.run(&mut t).await { + Ok(Some(res)) => Output::item(res), + Ok(None) => Output::default(), + Err(e) => { + tracing::error!("{}", e); + t.rollback().await.ok(); + return Output::error(Error::DeleteItem(item.id)); + } + } + } + Some(item) => { + let dbm = UpdateShoppingCartItem { + id: item.id, + product_id: msg.product_id, + shopping_cart_id: cart.id, + quantity: msg.quantity, + quantity_unit: msg.quantity_unit, + }; + match dbm.run(&mut t).await { + Ok(res) => Output::item(res), + Err(e) => { + tracing::error!("{}", e); + t.rollback().await.ok(); + return Output::error(Error::ModifyItem(item.id)); + } + } + } + None => { + let dbm = CreateShoppingCartItem { + product_id: msg.product_id, + shopping_cart_id: cart.id, + quantity: msg.quantity, + quantity_unit: msg.quantity_unit, + }; + match dbm.run(&mut t).await { + Ok(res) => Output::item(res), + Err(e) => { + tracing::error!("{}", e); + t.rollback().await.ok(); + return Output::error(Error::CreateItem); + } + } + } + }; + + end_t!(t, res) +} + +pub async fn remove_product( + msg: carts::remove_product::Input, + db: Database, +) -> carts::remove_product::Output { + use carts::remove_product::Output; + let dbm = RemoveCartItem { + shopping_cart_id: msg.shopping_cart_id, + shopping_cart_item_id: Some(msg.shopping_cart_item_id), + product_id: None, + }; + let mut t = begin_t!(db); + + let res = match dbm.run(&mut t).await { + Ok(Some(res)) => Output::item(res), + Ok(None) => Output::default(), + Err(e) => { + tracing::error!("{}", e); + Output::error(Error::DeleteItem(msg.shopping_cart_item_id)) + } + }; + end_t!(t, res) +} + +pub async fn modify_cart( + msg: carts::modify_cart::Input, + db: Database, +) -> carts::modify_cart::Output { + use carts::modify_cart::Output; + + tracing::debug!("{:?}", msg); + + let mut t = begin_t!(db); + + let dbm = EnsureActiveShoppingCart { + buyer_id: msg.buyer_id, + }; + let cart = match dbm.run(&mut t).await { + Ok(res) => res, + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::InternalServerError); + } + }; + let dbm = UpdateShoppingCart { + id: cart.id, + buyer_id: msg.buyer_id, + payment_method: msg.payment_method.unwrap_or(cart.payment_method), + state: model::ShoppingCartState::Active, + checkout_notes: if msg.checkout_notes.is_empty() { + None + } else { + Some(msg.checkout_notes) + }, + }; + let cart: model::ShoppingCart = match dbm.run(&mut t).await { + Ok(res) => res, + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::ModifyCart(cart.id)); + } + }; + + let existing = + msg.items + .iter() + .fold(HashSet::with_capacity(msg.items.len()), |mut agg, item| { + agg.insert(item.product_id); + agg + }); + + let dbm = CartItems { + shopping_cart_id: cart.id, + }; + let items: Vec = match dbm.run(&mut t).await { + Ok(v) => v, + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::LoadItems(cart.id)); + } + }; + + for item in items + .into_iter() + .filter(|item| !existing.contains(&item.product_id)) + { + let dbm = RemoveCartItem { + shopping_cart_id: cart.id, + shopping_cart_item_id: Some(item.id), + product_id: None, + }; + match dbm.run(&mut t).await { + Ok(_) => {} + Err(e) => { + tracing::error!("{}", e); + return Output::error(Error::DeleteItem(item.id)); + } + }; + } + + let mut out = Vec::with_capacity(msg.items.len()); + + for item in msg.items { + let res = modify_item(item, db.clone()).await; + if let carts::modify_item::Output { + error: Some(error), .. + } = &res + { + return Output::error(error.clone()); + } + if let Some(item) = res.item { + out.push(item); + } + } + + end_t!( + t, + Output::cart(CartDetails { + cart_id: cart.id, + items: out, + checkout_notes: cart.checkout_notes.unwrap_or_default(), + payment_method: cart.payment_method, + }) + ) +} diff --git a/crates/cart_manager/src/db/mod.rs b/crates/cart_manager/src/db/mod.rs new file mode 100644 index 0000000..99fc375 --- /dev/null +++ b/crates/cart_manager/src/db/mod.rs @@ -0,0 +1,32 @@ +pub mod shopping_cart_items; +pub mod shopping_carts; + +use config::SharedAppConfig; +pub use shopping_cart_items::*; +pub use shopping_carts::*; +use sqlx_core::pool::Pool; +use sqlx_core::postgres::Postgres; + +#[derive(Clone)] +pub struct Database { + pub pool: sqlx::PgPool, + _config: SharedAppConfig, +} + +impl Database { + pub async fn build(config: SharedAppConfig) -> Self { + let url = config.lock().cart_manager().database_url.clone(); + let pool = sqlx::PgPool::connect(&url).await.unwrap_or_else(|e| { + tracing::error!("Failed to connect to database. {e:?}"); + std::process::exit(1); + }); + Self { + pool, + _config: config, + } + } + + pub fn pool(&self) -> Pool { + self.pool.clone() + } +} diff --git a/crates/cart_manager/src/db/shopping_cart_items.rs b/crates/cart_manager/src/db/shopping_cart_items.rs new file mode 100644 index 0000000..89e1dab --- /dev/null +++ b/crates/cart_manager/src/db/shopping_cart_items.rs @@ -0,0 +1,631 @@ +use model::*; + +pub type Result = std::result::Result; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Can't create shopping cart item")] + CantCreate, + #[error("Can't update shopping cart item")] + CantUpdate(ShoppingCartItemId), + #[error("Shopping cart does not exists")] + NotExists, + #[error("Failed to load all shopping cart items")] + All, + #[error("Failed to load account shopping cart items")] + AccountCarts, + #[error("Failed to load items for shopping cart {0}")] + CartItems(ShoppingCartId), + #[error("Can't find shopping cart item doe to lack of identity")] + NoIdentity, + #[error("Failed to update shopping cart item with id {shopping_cart_item_id:?} and/or product id {product_id:?}")] + Update { + shopping_cart_item_id: Option, + product_id: Option, + }, +} + +#[derive(Debug)] +pub struct AllShoppingCartItems; + +impl AllShoppingCartItems { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + sqlx::query_as( + r#" +SELECT shopping_cart_items.id, + shopping_cart_items.product_id, + shopping_cart_items.shopping_cart_id, + shopping_cart_items.quantity, + shopping_cart_items.quantity_unit +FROM shopping_cart_items +INNER JOIN shopping_carts + ON shopping_cart_items.shopping_cart_id = shopping_carts.id +ORDER BY shopping_cart_items.id ASC + "#, + ) + .fetch_all(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::All + }) + } +} + +#[derive(Debug)] +pub struct AccountShoppingCartItems { + pub account_id: AccountId, + pub shopping_cart_id: Option, +} + +impl AccountShoppingCartItems { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + let msg = self; + match msg.shopping_cart_id { + Some(shopping_cart_id) => sqlx::query_as( + r#" +SELECT shopping_cart_items.id as id, + shopping_cart_items.product_id as product_id, + shopping_cart_items.shopping_cart_id as shopping_cart_id, + shopping_cart_items.quantity as quantity, + shopping_cart_items.quantity_unit as quantity_unit +FROM shopping_cart_items +LEFT JOIN shopping_carts + ON shopping_carts.id = shopping_cart_id +WHERE shopping_carts.buyer_id = $1 AND shopping_carts.id = $2 + "#, + ) + .bind(msg.account_id) + .bind(shopping_cart_id), + None => sqlx::query_as( + r#" +SELECT shopping_cart_items.id as id, + shopping_cart_items.product_id as product_id, + shopping_cart_items.shopping_cart_id as shopping_cart_id, + shopping_cart_items.quantity as quantity, + shopping_cart_items.quantity_unit as quantity_unit +FROM shopping_cart_items +LEFT JOIN shopping_carts + ON shopping_carts.id = shopping_cart_id +WHERE shopping_carts.buyer_id = $1 + "#, + ) + .bind(msg.account_id), + } + .fetch_all(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::AccountCarts + }) + } +} + +#[derive(Debug)] +pub struct CreateShoppingCartItem { + pub product_id: ProductId, + pub shopping_cart_id: ShoppingCartId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +impl CreateShoppingCartItem { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +INSERT INTO shopping_cart_items (product_id, shopping_cart_id, quantity, quantity_unit) +VALUES ($1, $2, $3, $4) +RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit + "#, + ) + .bind(msg.product_id) + .bind(msg.shopping_cart_id) + .bind(msg.quantity) + .bind(msg.quantity_unit) + .fetch_one(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(&e); + Error::CantCreate + }) + } +} + +#[derive(Debug)] +pub struct UpdateShoppingCartItem { + pub id: ShoppingCartItemId, + pub product_id: ProductId, + pub shopping_cart_id: ShoppingCartId, + pub quantity: Quantity, + pub quantity_unit: QuantityUnit, +} + +impl UpdateShoppingCartItem { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +UPDATE shopping_cart_items +SET product_id = $2, shopping_cart_id = $3, quantity = $4, quantity_unit = $5 +WHERE id = $1 +RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit + "#, + ) + .bind(msg.id) + .bind(msg.product_id) + .bind(msg.shopping_cart_id) + .bind(msg.quantity) + .bind(msg.quantity_unit) + .fetch_one(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantUpdate(msg.id) + }) + } +} + +#[derive(Debug)] +pub struct DeleteShoppingCartItem { + pub id: ShoppingCartItemId, +} + +impl DeleteShoppingCartItem { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + let msg = self; + sqlx::query_as( + r#" +DELETE FROM shopping_cart_items +WHERE id = $1 +RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit + "#, + ) + .bind(msg.id) + .fetch_optional(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantUpdate(msg.id) + }) + } +} + +#[derive(Debug)] +pub struct FindShoppingCartItem { + pub id: ShoppingCartItemId, +} + +impl FindShoppingCartItem { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +SELECT id, product_id, shopping_cart_id, quantity, quantity_unit +FROM shopping_cart_items +WHERE id = $1 + "#, + ) + .bind(msg.id) + .fetch_one(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::NotExists + }) + } +} + +#[derive(Debug)] +pub struct ActiveCartItemByProduct { + pub product_id: ProductId, +} + +impl ActiveCartItemByProduct { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + let msg = self; + sqlx::query_as( + r#" +SELECT shopping_cart_items.id, + shopping_cart_items.product_id, + shopping_cart_items.shopping_cart_id, + shopping_cart_items.quantity, + shopping_cart_items.quantity_unit +FROM shopping_cart_items +INNER JOIN shopping_carts + ON shopping_cart_items.shopping_cart_id = shopping_carts.id +WHERE product_id = $1 + AND shopping_carts.state = $2 +ORDER BY shopping_cart_items.id ASC + "#, + ) + .bind(msg.product_id) + .bind(model::ShoppingCartState::Active) + .fetch_optional(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::NotExists + }) + } +} + +#[derive(Debug)] +pub struct CartItems { + pub shopping_cart_id: ShoppingCartId, +} + +impl CartItems { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + let msg = self; + let shopping_cart_id = msg.shopping_cart_id; + sqlx::query_as( + r#" +SELECT id, + product_id, + shopping_cart_id, + quantity, + quantity_unit +FROM shopping_cart_items +WHERE shopping_cart_id = $1 +ORDER BY shopping_cart_items.id ASC + "#, + ) + .bind(msg.shopping_cart_id) + .fetch_all(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CartItems(shopping_cart_id) + }) + } +} + +#[derive(Debug)] +pub struct RemoveCartItem { + pub shopping_cart_id: ShoppingCartId, + pub shopping_cart_item_id: Option, + pub product_id: Option, +} + +impl RemoveCartItem { + pub async fn run( + self, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result> { + let msg = self; + match (msg.shopping_cart_item_id, msg.product_id) { + (Some(shopping_cart_item_id), None) => sqlx::query_as( + r#" +DELETE FROM shopping_cart_items +WHERE shopping_cart_id = $1 AND id = $2 +RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit + "#, + ) + .bind(msg.shopping_cart_id) + .bind(shopping_cart_item_id), + (Some(shopping_cart_item_id), Some(product_id)) => sqlx::query_as( + r#" +DELETE FROM shopping_cart_items +WHERE shopping_cart_id = $1 AND id = $2 AND product_id = $3 +RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit + "#, + ) + .bind(msg.shopping_cart_id) + .bind(shopping_cart_item_id) + .bind(product_id), + (None, Some(product_id)) => sqlx::query_as( + r#" +DELETE FROM shopping_cart_items +WHERE shopping_cart_id = $1 AND product_id = $2 +RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit + "#, + ) + .bind(msg.shopping_cart_id) + .bind(product_id), + _ => return Err(Error::NoIdentity), + } + .fetch_optional(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Update { + shopping_cart_item_id: msg.shopping_cart_item_id, + product_id: msg.product_id, + } + }) + } +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use fake::Fake; + use model::*; + use uuid::Uuid; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use super::*; + + async fn test_product(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { + CreateProduct { + name: ProductName::new(format!("{}", Uuid::new_v4())), + short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), + long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), + category: None, + price: Price::from_u32(4687), + deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), + } + .run(t) + .await + .unwrap() + } + + async fn test_account( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + login: Option, + email: Option, + hash: Option, + ) -> FullAccount { + use fake::faker::internet::en; + let login: String = login.unwrap_or_else(|| en::Username().fake()); + let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); + let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); + + CreateAccount { + email: Email::new(email), + login: Login::new(login), + pass_hash: PassHash::new(hash), + role: Role::Admin, + } + .run(t) + .await + .unwrap() + } + + async fn test_shopping_cart( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + buyer_id: Option, + state: ShoppingCartState, + ) -> ShoppingCart { + let buyer_id = match buyer_id { + Some(id) => id, + _ => test_account(&mut *t, None, None, None).await.id, + }; + + sqlx::query( + r#" +UPDATE shopping_carts +SET state = 'closed' +WHERE buyer_id = $1 + "#, + ) + .bind(buyer_id) + .execute(&mut *t) + .await + .unwrap(); + + let cart = CreateShoppingCart { + buyer_id, + payment_method: PaymentMethod::PaymentOnTheSpot, + } + .run(&mut *t) + .await + .unwrap(); + + UpdateShoppingCart { + id: cart.id, + buyer_id: cart.buyer_id, + payment_method: cart.payment_method, + state, + checkout_notes: None, + } + .run(&mut *t) + .await + .unwrap() + } + + async fn test_shopping_cart_item( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + shopping_cart_id: Option, + product_id: Option, + ) -> ShoppingCartItem { + let shopping_cart_id = match shopping_cart_id { + Some(id) => id, + _ => { + test_shopping_cart(&mut *t, None, ShoppingCartState::Closed) + .await + .id + } + }; + let product_id = match product_id { + Some(id) => id, + _ => test_product(&mut *t).await.id, + }; + CreateShoppingCartItem { + product_id, + shopping_cart_id, + quantity: Quantity::from_u32(496879), + quantity_unit: QuantityUnit::Gram, + } + .run(t) + .await + .unwrap() + } + + #[actix::test] + async fn create() { + testx::db_t_ref!(t); + + test_shopping_cart_item(&mut t, None, None).await; + + testx::db_rollback!(t); + } + + #[actix::test] + async fn all() { + testx::db_t_ref!(t); + + let account_id = test_account(&mut t, None, None, None).await.id; + + let mut items = Vec::with_capacity(9); + + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + + let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + + let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + + let all = all_shopping_cart_items(AllShoppingCartItems, &mut t) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, items) + } + + #[actix::test] + async fn account_cart_with_cart_id() { + testx::db_t_ref!(t); + + let account_id = test_account(&mut t, None, None, None).await.id; + + let mut items = Vec::with_capacity(9); + + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + + let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + + let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + test_shopping_cart_item(&mut t, Some(cart3.id), None).await; + + let all = account_shopping_cart_items( + AccountShoppingCartItems { + account_id, + shopping_cart_id: Some(cart2.id), + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, items) + } + + #[actix::test] + async fn account_cart_without_cart_id() { + testx::db_t_ref!(t); + + let account_id = test_account(&mut t, None, None, None).await.id; + + let mut items = Vec::with_capacity(9); + + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart1.id), None).await); + + let cart2 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Active).await; + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart2.id), None).await); + + let cart3 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + items.push(test_shopping_cart_item(&mut t, Some(cart3.id), None).await); + + let all = account_shopping_cart_items( + AccountShoppingCartItems { + account_id, + shopping_cart_id: None, + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_eq!(all, items) + } + + #[actix::test] + async fn update() { + testx::db_t_ref!(t); + let account_id = test_account(&mut t, None, None, None).await.id; + let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; + let item = test_shopping_cart_item(&mut t, Some(cart1.id), None).await; + + let updated = update_shopping_cart_item( + UpdateShoppingCartItem { + id: item.id, + product_id: item.product_id, + shopping_cart_id: item.shopping_cart_id, + quantity: Quantity::from_u32(987979879), + quantity_unit: QuantityUnit::Kilogram, + }, + &mut t, + ) + .await + .unwrap(); + + assert_ne!(item, updated); + assert_eq!( + updated, + ShoppingCartItem { + id: item.id, + product_id: item.product_id, + shopping_cart_id: item.shopping_cart_id, + quantity: Quantity::from_u32(987979879), + quantity_unit: QuantityUnit::Kilogram, + } + ); + } +} diff --git a/crates/cart_manager/src/db/shopping_carts.rs b/crates/cart_manager/src/db/shopping_carts.rs new file mode 100644 index 0000000..6fc15c9 --- /dev/null +++ b/crates/cart_manager/src/db/shopping_carts.rs @@ -0,0 +1,423 @@ +use model::*; +use sqlx::PgPool; + +pub type Result = std::result::Result; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Can't create shopping cart")] + CantCreate, + #[error("Can't update shopping cart {0}")] + CantUpdate(ShoppingCartId), + #[error("Shopping cart does not exists")] + NotExists, + #[error("Failed to load all shopping carts")] + All, + #[error("Failed to load account shopping carts")] + AccountCarts, +} + +#[derive(Debug)] +pub struct AllShoppingCarts; + +impl AllShoppingCarts { + pub async fn run(self, pool: PgPool) -> Result> { + sqlx::query_as( + r#" +SELECT id, buyer_id, payment_method, state, checkout_notes +FROM shopping_carts + "#, + ) + .fetch_all(&pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::All + }) + } +} + +#[derive(Debug)] +pub struct AccountShoppingCarts { + pub account_id: AccountId, + pub state: Option, +} + +impl AccountShoppingCarts { + pub async fn run(self, pool: PgPool) -> Result> { + let msg = self; + if let Some(state) = msg.state { + sqlx::query_as( + r#" +SELECT id, buyer_id, payment_method, state, checkout_notes +FROM shopping_carts +WHERE buyer_id = $1 AND state = $2 +"#, + ) + .bind(msg.account_id) + .bind(state) + } else { + sqlx::query_as( + r#" +SELECT id, buyer_id, payment_method, state, checkout_notes +FROM shopping_carts +WHERE buyer_id = $1 + "#, + ) + .bind(msg.account_id) + } + .fetch_all(&pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::AccountCarts + }) + } +} + +#[derive(Debug)] +pub struct CreateShoppingCart { + pub buyer_id: AccountId, + pub payment_method: PaymentMethod, +} + +impl CreateShoppingCart { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +INSERT INTO shopping_carts (buyer_id, payment_method) +VALUES ($1, $2) +RETURNING id, buyer_id, payment_method, state, checkout_notes + "#, + ) + .bind(msg.buyer_id) + .bind(msg.payment_method) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::CantCreate + }) + } +} + +#[derive(Debug)] +pub struct UpdateShoppingCart { + pub id: ShoppingCartId, + pub buyer_id: AccountId, + pub payment_method: PaymentMethod, + pub state: ShoppingCartState, + pub checkout_notes: Option, +} + +impl UpdateShoppingCart { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +UPDATE shopping_carts +SET buyer_id = $2, payment_method = $3, state = $4, checkout_notes = $5 +WHERE id = $1 +RETURNING id, buyer_id, payment_method, state, checkout_notes + "#, + ) + .bind(msg.id) + .bind(msg.buyer_id) + .bind(msg.payment_method) + .bind(msg.state) + .bind(msg.checkout_notes) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantUpdate(msg.id) + }) + } +} + +#[derive(Debug)] +pub struct ShoppingCartSetState { + pub id: ShoppingCartId, + pub state: ShoppingCartState, + pub checkout_notes: Option, +} + +impl ShoppingCartSetState { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +UPDATE shopping_carts +SET state = $2, checkout_notes = $3 +WHERE id = $1 +RETURNING id, buyer_id, payment_method, state, checkout_notes + "#, + ) + .bind(msg.id) + .bind(msg.state) + .bind(msg.checkout_notes) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::CantUpdate(msg.id) + }) + } +} + +#[derive(Debug)] +pub struct FindShoppingCart { + pub id: ShoppingCartId, +} + +impl FindShoppingCart { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + sqlx::query_as( + r#" +SELECT id, buyer_id, payment_method, state, checkout_notes +FROM shopping_carts +WHERE id = $1 + "#, + ) + .bind(msg.id) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::NotExists + }) + } +} + +#[derive(Debug)] +pub struct EnsureActiveShoppingCart { + pub buyer_id: AccountId, +} + +impl EnsureActiveShoppingCart { + pub async fn run( + self, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { + let msg = self; + if let Ok(Some(cart)) = sqlx::query_as( + r#" +INSERT INTO shopping_carts (buyer_id, state) +VALUES ($1, 'active') +ON CONFLICT + DO NOTHING +RETURNING id, buyer_id, payment_method, state, checkout_notes + "#, + ) + .bind(msg.buyer_id) + .fetch_optional(&mut *pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::NotExists + }) { + return Ok(cart); + }; + sqlx::query_as( + r#" +SELECT id, buyer_id, payment_method, state, checkout_notes +FROM shopping_carts +WHERE buyer_id = $1 AND state = 'active' + "#, + ) + .bind(msg.buyer_id) + .fetch_one(pool) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::NotExists + }) + } +} + +#[cfg(test)] +mod tests { + use config::UpdateConfig; + use fake::Fake; + use model::*; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + use super::*; + + async fn test_account( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + login: Option, + email: Option, + hash: Option, + ) -> FullAccount { + use fake::faker::internet::en; + let login: String = login.unwrap_or_else(|| en::Username().fake()); + let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); + let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); + + CreateAccount { + email: Email::new(email), + login: Login::new(login), + pass_hash: PassHash::new(hash), + role: Role::Admin, + } + .run(t) + .await + .unwrap() + } + + async fn test_shopping_cart( + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, + buyer_id: Option, + ) -> ShoppingCart { + let buyer_id = match buyer_id { + Some(id) => id, + _ => test_account(&mut *t, None, None, None).await.id, + }; + + super::create_shopping_cart( + CreateShoppingCart { + buyer_id, + payment_method: PaymentMethod::PaymentOnTheSpot, + }, + t, + ) + .await + .unwrap() + } + + #[actix::test] + async fn create_shopping_cart() { + testx::db_t_ref!(t); + + let account = test_account(&mut t, None, None, None).await; + + let cart = super::create_shopping_cart( + CreateShoppingCart { + buyer_id: account.id, + payment_method: PaymentMethod::PaymentOnTheSpot, + }, + &mut t, + ) + .await; + + testx::db_rollback!(t); + assert!(cart.is_ok()); + } + + #[actix::test] + async fn update_shopping_cart() { + testx::db_t_ref!(t); + + let account = test_account(&mut t, None, None, None).await; + + let original = test_shopping_cart(&mut t, Some(account.id)).await; + + let cart = super::update_shopping_cart( + UpdateShoppingCart { + id: original.id, + buyer_id: account.id, + payment_method: PaymentMethod::PayU, + state: ShoppingCartState::Closed, + checkout_notes: Some("Foo bar".into()), + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(cart, original); + assert_eq!( + cart, + ShoppingCart { + id: original.id, + buyer_id: account.id, + payment_method: PaymentMethod::PayU, + state: ShoppingCartState::Closed, + checkout_notes: Some("Foo bar".into()) + } + ); + } + + #[actix::test] + async fn without_cart_ensure_shopping_cart() { + testx::db_t_ref!(t); + + let account = test_account(&mut t, None, None, None).await; + + let cart = super::ensure_active_shopping_cart( + EnsureActiveShoppingCart { + buyer_id: account.id, + }, + &mut t, + ) + .await + .unwrap(); + + let id = cart.id; + + testx::db_rollback!(t); + assert_eq!( + cart, + model::ShoppingCart { + id, + buyer_id: account.id, + payment_method: Default::default(), + state: ShoppingCartState::Active, + checkout_notes: None + } + ); + } + + #[actix::test] + async fn with_inactive_cart_ensure_shopping_cart() { + testx::db_t_ref!(t); + + let account = test_account(&mut t, None, None, None).await; + + let original = test_shopping_cart(&mut t, Some(account.id)).await; + let _ = super::update_shopping_cart( + UpdateShoppingCart { + id: original.id, + buyer_id: account.id, + payment_method: Default::default(), + state: ShoppingCartState::Closed, + checkout_notes: None, + }, + &mut t, + ) + .await + .unwrap(); + + let cart = super::ensure_active_shopping_cart( + EnsureActiveShoppingCart { + buyer_id: account.id, + }, + &mut t, + ) + .await + .unwrap(); + + testx::db_rollback!(t); + assert_ne!(original, cart); + } +} diff --git a/crates/cart_manager/src/main.rs b/crates/cart_manager/src/main.rs new file mode 100644 index 0000000..72cbdc8 --- /dev/null +++ b/crates/cart_manager/src/main.rs @@ -0,0 +1,50 @@ +use config::UpdateConfig; +use tracing_subscriber::fmt::format::FmtSpan; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +use crate::db::Database; + +pub mod actions; +pub mod db; +pub mod mqtt; +pub mod rpc; + +pub struct Opts {} + +impl UpdateConfig for Opts {} + +#[actix::main] +async fn main() { + dotenv::dotenv().ok(); + init_tracing("account-manager"); + + let opts = Opts {}; + + let config = config::default_load(&opts); + + let db = Database::build(config.clone()).await; + + let mqtt_client = mqtt::start(config.clone(), db.clone()).await; + rpc::start(config.clone(), db.clone(), mqtt_client.clone()).await; +} + +pub fn init_tracing(_service_name: &str) { + std::env::set_var("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "12"); + + let tracer = { + use opentelemetry::sdk::export::trace::stdout::new_pipeline; + use opentelemetry::sdk::trace::Config; + new_pipeline() + .with_trace_config(Config::default()) + .with_pretty_print(true) + .install_simple() + }; + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)) + .with(tracing_opentelemetry::layer().with_tracer(tracer)) + .try_init() + .unwrap(); +} diff --git a/crates/cart_manager/src/mqtt.rs b/crates/cart_manager/src/mqtt.rs new file mode 100644 index 0000000..01a1ee6 --- /dev/null +++ b/crates/cart_manager/src/mqtt.rs @@ -0,0 +1,37 @@ +use std::time::Duration; + +use config::SharedAppConfig; +use rumqttc::{Event, Incoming}; + +use crate::Database; + +pub async fn start(config: SharedAppConfig, _db: Database) -> channels::AsyncClient { + let mut mqtt_options = { + let l = config.lock(); + let bind = &l.account_manager().mqtt_bind; + let port = l.account_manager().mqtt_port; + tracing::info!("Starting account mqtt at {}:{}", bind, port); + + rumqttc::MqttOptions::new(channels::accounts::CLIENT_NAME, bind, port) + }; + mqtt_options.set_keep_alive(Duration::from_secs(5)); + + let (client, mut event_loop) = rumqttc::AsyncClient::new(mqtt_options, 10); + + let client = channels::AsyncClient(client); + let spawn_client = client.clone(); + tokio::spawn(async move { + let _client = spawn_client.clone(); + loop { + let notification = event_loop.poll().await; + + match notification { + Ok(Event::Incoming(Incoming::Publish(publish))) => match publish.topic.as_str() { + _ => {} + }, + _ => {} + } + } + }); + client +} diff --git a/crates/cart_manager/src/rpc.rs b/crates/cart_manager/src/rpc.rs new file mode 100644 index 0000000..4688dfe --- /dev/null +++ b/crates/cart_manager/src/rpc.rs @@ -0,0 +1,79 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use channels::carts::modify_item::{Input, Output}; +use channels::carts::rpc::Carts; +use channels::AsyncClient; +use config::SharedAppConfig; +use futures::{future, StreamExt}; +use tarpc::server::incoming::Incoming; +use tarpc::server::Channel; +use tarpc::tokio_serde::formats::Bincode; +use tarpc::{context, server}; + +use crate::db::Database; + +#[derive(Clone)] +pub struct CartsServer { + db: Database, + _config: SharedAppConfig, + _mqtt_client: AsyncClient, +} + +#[tarpc::server] +impl Carts for CartsServer { + async fn modify_item(self, _: context::Context, input: Input) -> Output { + crate::actions::modify_item(input, self.db).await + } + + async fn modify_cart( + self, + _: context::Context, + input: channels::carts::modify_cart::Input, + ) -> channels::carts::modify_cart::Output { + crate::actions::modify_cart(input, self.db).await + } + + async fn remove_cart( + self, + _: context::Context, + input: channels::carts::remove_product::Input, + ) -> channels::carts::remove_product::Output { + crate::actions::remove_product(input, self.db).await + } +} + +pub async fn start(config: SharedAppConfig, db: Database, mqtt_client: AsyncClient) { + let port = { config.lock().cart_manager().port }; + + let server_addr = (IpAddr::V4(Ipv4Addr::LOCALHOST), port); + + let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Bincode::default) + .await + .unwrap(); + tracing::info!("Starting account rpc at {}", listener.local_addr()); + listener.config_mut().max_frame_length(usize::MAX); + listener + // Ignore accept errors. + .filter_map(|r| future::ready(r.ok())) + .map(server::BaseChannel::with_defaults) + // Limit channels to 8 per IP. + .max_channels_per_key(8, |t| t.transport().peer_addr().unwrap().ip()) + .max_concurrent_requests_per_channel(20) + // serve is generated by the service attribute. It takes as input any type implementing + // the generated World trait. + .map(|channel| { + channel.execute( + CartsServer { + db: db.clone(), + _config: config.clone(), + _mqtt_client: mqtt_client.clone(), + } + .serve(), + ) + }) + // Max 10 channels. + .buffer_unordered(10) + .for_each(|_| async {}) + .await; + tracing::info!("RPC channel closed"); +} diff --git a/shared/channels/Cargo.toml b/crates/channels/Cargo.toml similarity index 100% rename from shared/channels/Cargo.toml rename to crates/channels/Cargo.toml diff --git a/crates/channels/src/accounts.rs b/crates/channels/src/accounts.rs new file mode 100644 index 0000000..c392793 --- /dev/null +++ b/crates/channels/src/accounts.rs @@ -0,0 +1,149 @@ +#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)] +pub enum Error { + #[error("mqtt payload has invalid create account data")] + InvalidCreateAccount, + #[error("mqtt payload has invalid account failure data")] + InvalidAccountFailure, + #[error("Account does not exists")] + Account, + #[error("Account does have any addresses")] + Addresses, +} + +pub static CLIENT_NAME: &str = "account-manager"; + +#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum Topic { + CreateAccount, + AccountCreated, + SignUpFailure, +} + +impl Into for Topic { + fn into(self) -> String { + String::from(self.to_str()) + } +} + +impl<'s> PartialEq<&'s str> for Topic { + fn eq(&self, other: &&'s str) -> bool { + self.to_str() == *other + } +} + +impl PartialEq for Topic { + fn eq(&self, other: &String) -> bool { + self.to_str() == other.as_str() + } +} + +impl Topic { + pub fn to_str(self) -> &'static str { + match self { + Topic::CreateAccount => "account/create", + Topic::AccountCreated => "account/created", + Topic::SignUpFailure => "account/failure", + } + } +} + +pub mod register { + use model::{Email, Login, Password, Role}; + + use crate::accounts::Error; + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct Input { + pub email: Email, + pub login: Login, + pub password: Password, + pub role: Role, + } + + impl TryFrom for Input { + type Error = Error; + + fn try_from(value: bytes::Bytes) -> Result { + bincode::deserialize(value.as_ref()).map_err(|e| { + tracing::error!("{}", e); + Error::InvalidCreateAccount + }) + } + } + + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + pub struct Output { + pub account: Option, + pub error: Option, + } +} + +// #[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)] +// pub enum AccountFailure { +// #[error("Failed to hash password")] +// FailedToHashPassword, +// #[error("Failed to save account")] +// SaveAccount, +// #[error("Internal server error")] +// InternalServerError, +// } +// +// impl TryFrom for AccountFailure { +// type Error = Error; +// +// fn try_from(value: bytes::Bytes) -> Result { +// bincode::deserialize(value.as_ref()).map_err(|e| { +// tracing::error!("{}", e); +// Error::InvalidAccountFailure +// }) +// } +// } + +pub mod me { + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct Input { + pub account_id: model::AccountId, + } + + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + pub struct Output { + pub account: Option, + pub addresses: Option>, + pub error: Option, + } +} + +pub mod rpc { + use config::SharedAppConfig; + + use crate::accounts::{me, register}; + + #[tarpc::service] + pub trait Accounts { + /// Returns a greeting for name. + async fn me(input: me::Input) -> me::Output; + + /// Creates new user account. + async fn register_account(input: register::Input) -> register::Output; + } + + pub async fn create_client(config: SharedAppConfig) -> AccountsClient { + use tarpc::client; + use tarpc::tokio_serde::formats::Bincode; + + let addr = { + let l = config.lock(); + (l.account_manager().bind.clone(), l.account_manager().port) + }; + + let transport = tarpc::serde_transport::tcp::connect(addr, Bincode::default); + + let client = AccountsClient::new( + client::Config::default(), + transport.await.expect("Failed to connect to server"), + ) + .spawn(); + + client + } +} diff --git a/crates/channels/src/carts.rs b/crates/channels/src/carts.rs new file mode 100644 index 0000000..7be0483 --- /dev/null +++ b/crates/channels/src/carts.rs @@ -0,0 +1,147 @@ +pub static CLIENT_NAME: &str = "cart-manager"; + +pub enum Topic {} + +#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)] +pub enum Error { + #[error("Internal server error")] + InternalServerError, + #[error("Failed to load account shopping carts")] + NoCarts, + #[error("Account does not have active shopping cart")] + NoActiveCart, + #[error("Failed to delete item {0:?}")] + DeleteItem(model::ShoppingCartItemId), + #[error("Failed to modify item {0:?}")] + ModifyItem(model::ShoppingCartItemId), + #[error("Failed to create item")] + CreateItem, + #[error("Failed to modify cart {0:?}")] + ModifyCart(model::ShoppingCartId), + #[error("Failed to load cart {0:?} items")] + LoadItems(model::ShoppingCartId), +} + +pub mod remove_product { + use super::Error; + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct Input { + pub shopping_cart_id: model::ShoppingCartId, + pub shopping_cart_item_id: model::ShoppingCartItemId, + } + + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + pub struct Output { + pub item: Option, + pub error: Option, + } + + impl Output { + pub fn error(error: Error) -> Self { + Self { + error: Some(error), + ..Default::default() + } + } + + pub fn item(item: model::ShoppingCartItem) -> Self { + Self { + item: Some(item), + ..Default::default() + } + } + } +} + +pub mod modify_item { + use super::Error; + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct Input { + pub buyer_id: model::AccountId, + pub product_id: model::ProductId, + pub quantity: model::Quantity, + pub quantity_unit: model::QuantityUnit, + } + + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + pub struct Output { + pub item: Option, + pub error: Option, + } + + impl Output { + pub fn error(error: Error) -> Self { + Self { + error: Some(error), + ..Default::default() + } + } + + pub fn item(item: model::ShoppingCartItem) -> Self { + Self { + item: Some(item), + ..Default::default() + } + } + } +} + +pub mod modify_cart { + use super::{modify_item, Error}; + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct Input { + pub buyer_id: model::AccountId, + pub items: Vec, + pub checkout_notes: String, + pub payment_method: Option, + } + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct CartDetails { + pub cart_id: model::ShoppingCartId, + pub items: Vec, + pub checkout_notes: String, + pub payment_method: model::PaymentMethod, + } + + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + pub struct Output { + pub cart: Option, + pub error: Option, + } + + impl Output { + pub fn error(error: Error) -> Self { + Self { + error: Some(error), + ..Default::default() + } + } + + pub fn cart(cart: CartDetails) -> Self { + Self { + cart: Some(cart), + ..Default::default() + } + } + } +} + +pub mod rpc { + use super::{modify_cart, modify_item, remove_product}; + + #[tarpc::service] + pub trait Carts { + /// Change shopping cart item. + async fn modify_item(input: modify_item::Input) -> modify_item::Output; + + /// Change entire shopping cart content. + async fn modify_cart(input: modify_cart::Input) -> modify_cart::Output; + + /// Remove entire shopping cart. + async fn remove_cart(input: remove_product::Input) -> remove_product::Output; + } +} diff --git a/crates/channels/src/lib.rs b/crates/channels/src/lib.rs new file mode 100644 index 0000000..d6df606 --- /dev/null +++ b/crates/channels/src/lib.rs @@ -0,0 +1,33 @@ +#![feature(structural_match)] + +pub mod accounts; +pub mod carts; + +#[derive(Clone)] +pub struct AsyncClient(pub rumqttc::AsyncClient); + +impl AsyncClient { + pub async fn publish, T: serde::Serialize>( + &self, + topic: Topic, + qos: rumqttc::QoS, + retain: bool, + t: T, + ) -> Result<(), rumqttc::ClientError> { + let v = bincode::serialize(&t).unwrap_or_default(); + let bytes = bytes::Bytes::copy_from_slice(&v); + self.0.publish_bytes(topic, qos, retain, bytes).await + } + + pub async fn publish_or_log, T: serde::Serialize>( + &self, + topic: Topic, + qos: rumqttc::QoS, + retain: bool, + t: T, + ) { + if let Err(e) = self.publish(topic, qos, retain, t).await { + tracing::error!("{}", e); + } + } +} diff --git a/shared/config/Cargo.toml b/crates/config/Cargo.toml similarity index 98% rename from shared/config/Cargo.toml rename to crates/config/Cargo.toml index 52a85d9..a45e650 100644 --- a/shared/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -4,18 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] - actix-web = { version = "4.0", features = [] } - parking_lot = { version = "0.12", features = [] } - password-hash = { version = "0.4", features = ["alloc"] } - pay_u = { version = '0.1', features = ["single-client"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = [] } - thiserror = { version = "1.0" } toml = { version = "0.5", features = [] } - tracing = { version = "0.1.34" } diff --git a/shared/config/src/lib.rs b/crates/config/src/lib.rs similarity index 93% rename from shared/config/src/lib.rs rename to crates/config/src/lib.rs index 4a42f2c..55f9235 100644 --- a/shared/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -13,8 +13,10 @@ pub trait UpdateConfig { fn update_config(&self, _config: &mut AppConfig) {} } -trait Example: Sized { - fn example() -> Self; +trait Example: Sized + Default { + fn example() -> Self { + Self::default() + } } #[derive(Clone)] @@ -315,17 +317,7 @@ pub struct SearchConfig { search_active: bool, } -impl Example for SearchConfig { - fn example() -> Self { - Self { - sonic_search_addr: None, - sonic_search_pass: None, - sonic_ingest_addr: None, - sonic_ingest_pass: None, - search_active: true, - } - } -} +impl Example for SearchConfig {} impl Default for SearchConfig { fn default() -> Self { @@ -386,14 +378,7 @@ pub struct FilesConfig { local_path: Option, } -impl Example for FilesConfig { - fn example() -> Self { - Self { - public_path: Some("/uploads".into()), - local_path: Some("/var/local/bazzar".into()), - } - } -} +impl Example for FilesConfig {} impl Default for FilesConfig { fn default() -> Self { @@ -426,6 +411,9 @@ impl FilesConfig { pub struct AccountManagerConfig { pub port: u16, pub bind: String, + pub mqtt_port: u16, + pub mqtt_bind: String, + pub database_url: String, } impl Default for AccountManagerConfig { @@ -433,19 +421,38 @@ impl Default for AccountManagerConfig { Self { port: 19329, bind: "0.0.0.0".into(), + mqtt_port: 1883, + mqtt_bind: "0.0.0.0".into(), + database_url: "postgres://postgres@localhost/bazzar_accounts".into(), } } } -impl Example for AccountManagerConfig { - fn example() -> Self { +impl Example for AccountManagerConfig {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CartManagerConfig { + pub port: u16, + pub bind: String, + pub mqtt_port: u16, + pub mqtt_bind: String, + pub database_url: String, +} + +impl Default for CartManagerConfig { + fn default() -> Self { Self { - port: 19329, + port: 19330, bind: "0.0.0.0".into(), + mqtt_port: 1884, + mqtt_bind: "0.0.0.0".into(), + database_url: "postgres://postgres@localhost/bazzar_carts".into(), } } } +impl Example for CartManagerConfig {} + #[derive(Serialize, Deserialize)] pub struct AppConfig { #[serde(default)] @@ -462,6 +469,8 @@ pub struct AppConfig { files: FilesConfig, #[serde(default)] account_manager: AccountManagerConfig, + #[serde(default)] + cart_manager: CartManagerConfig, #[serde(skip)] config_path: String, } @@ -476,6 +485,7 @@ impl Example for AppConfig { search: SearchConfig::example(), files: FilesConfig::example(), account_manager: AccountManagerConfig::example(), + cart_manager: Default::default(), config_path: "".to_string(), } } @@ -525,6 +535,10 @@ impl AppConfig { pub fn account_manager(&self) -> &AccountManagerConfig { &self.account_manager } + + pub fn cart_manager(&self) -> &CartManagerConfig { + &self.cart_manager + } } impl Default for AppConfig { @@ -537,6 +551,7 @@ impl Default for AppConfig { search: Default::default(), files: FilesConfig::default(), account_manager: AccountManagerConfig::default(), + cart_manager: Default::default(), config_path: "".to_string(), } } diff --git a/actors/database_manager/Cargo.toml b/crates/database_manager/Cargo.toml similarity index 88% rename from actors/database_manager/Cargo.toml rename to crates/database_manager/Cargo.toml index 536b8f7..d15bb94 100644 --- a/actors/database_manager/Cargo.toml +++ b/crates/database_manager/Cargo.toml @@ -11,10 +11,10 @@ actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } async-trait = { version = "0.1.56" } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } +config = { path = "../config" } fake = { version = "2.4.3", features = ["derive", "chrono", "http", "uuid"], optional = true } itertools = { version = "0.10.3" } -model = { path = "../../shared/model" } +model = { path = "../model" } pretty_env_logger = { version = "0.4", features = [] } rand = { version = "0.8.5", optional = true } rumqttc = { version = "*" } @@ -26,4 +26,4 @@ tracing = { version = "0.1.34" } uuid = { version = "1.2.1", features = ["serde"] } [dev-dependencies] -testx = { path = "../../shared/testx" } +testx = { path = "../testx" } diff --git a/actors/database_manager/src/account_addresses.rs b/crates/database_manager/src/account_addresses.rs similarity index 100% rename from actors/database_manager/src/account_addresses.rs rename to crates/database_manager/src/account_addresses.rs diff --git a/actors/database_manager/src/accounts.rs b/crates/database_manager/src/accounts.rs similarity index 99% rename from actors/database_manager/src/accounts.rs rename to crates/database_manager/src/accounts.rs index 4309d31..2d1cad4 100644 --- a/actors/database_manager/src/accounts.rs +++ b/crates/database_manager/src/accounts.rs @@ -156,7 +156,7 @@ pub struct FindAccount { db_async_handler!(FindAccount, find_account, FullAccount, inner_find_account); -pub(crate) async fn find_account( +pub async fn find_account( msg: FindAccount, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { diff --git a/actors/database_manager/src/lib.rs b/crates/database_manager/src/lib.rs similarity index 100% rename from actors/database_manager/src/lib.rs rename to crates/database_manager/src/lib.rs diff --git a/actors/database_manager/src/order_addresses.rs b/crates/database_manager/src/order_addresses.rs similarity index 100% rename from actors/database_manager/src/order_addresses.rs rename to crates/database_manager/src/order_addresses.rs diff --git a/actors/database_manager/src/order_items.rs b/crates/database_manager/src/order_items.rs similarity index 100% rename from actors/database_manager/src/order_items.rs rename to crates/database_manager/src/order_items.rs diff --git a/actors/database_manager/src/orders.rs b/crates/database_manager/src/orders.rs similarity index 100% rename from actors/database_manager/src/orders.rs rename to crates/database_manager/src/orders.rs diff --git a/actors/database_manager/src/photos.rs b/crates/database_manager/src/photos.rs similarity index 100% rename from actors/database_manager/src/photos.rs rename to crates/database_manager/src/photos.rs diff --git a/actors/database_manager/src/product_photos.rs b/crates/database_manager/src/product_photos.rs similarity index 100% rename from actors/database_manager/src/product_photos.rs rename to crates/database_manager/src/product_photos.rs diff --git a/actors/database_manager/src/products.rs b/crates/database_manager/src/products.rs similarity index 100% rename from actors/database_manager/src/products.rs rename to crates/database_manager/src/products.rs diff --git a/actors/database_manager/src/shopping_cart_items.rs b/crates/database_manager/src/shopping_cart_items.rs similarity index 100% rename from actors/database_manager/src/shopping_cart_items.rs rename to crates/database_manager/src/shopping_cart_items.rs diff --git a/actors/database_manager/src/shopping_carts.rs b/crates/database_manager/src/shopping_carts.rs similarity index 98% rename from actors/database_manager/src/shopping_carts.rs rename to crates/database_manager/src/shopping_carts.rs index e6771a9..6c6b2dc 100644 --- a/actors/database_manager/src/shopping_carts.rs +++ b/crates/database_manager/src/shopping_carts.rs @@ -101,7 +101,7 @@ db_async_handler!( inner_create_shopping_cart ); -pub(crate) async fn create_shopping_cart( +pub async fn create_shopping_cart( msg: CreateShoppingCart, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { @@ -140,7 +140,7 @@ db_async_handler!( inner_update_shopping_cart ); -pub(crate) async fn update_shopping_cart( +pub async fn update_shopping_cart( msg: UpdateShoppingCart, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { @@ -180,7 +180,7 @@ db_async_handler!( inner_shopping_cart_set_state ); -pub(crate) async fn shopping_cart_set_state( +pub async fn shopping_cart_set_state( msg: ShoppingCartSetState, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { @@ -216,7 +216,7 @@ db_async_handler!( inner_find_shopping_cart ); -pub(crate) async fn find_shopping_cart( +pub async fn find_shopping_cart( msg: FindShoppingCart, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { @@ -249,7 +249,7 @@ db_async_handler!( inner_ensure_active_shopping_cart ); -pub(crate) async fn ensure_active_shopping_cart( +pub async fn ensure_active_shopping_cart( msg: EnsureActiveShoppingCart, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { diff --git a/actors/database_manager/src/stocks.rs b/crates/database_manager/src/stocks.rs similarity index 100% rename from actors/database_manager/src/stocks.rs rename to crates/database_manager/src/stocks.rs diff --git a/actors/database_manager/src/tokens.rs b/crates/database_manager/src/tokens.rs similarity index 100% rename from actors/database_manager/src/tokens.rs rename to crates/database_manager/src/tokens.rs diff --git a/db-seed/Cargo.toml b/crates/db-seed/Cargo.toml similarity index 70% rename from db-seed/Cargo.toml rename to crates/db-seed/Cargo.toml index 57b5a9d..d254e0c 100644 --- a/db-seed/Cargo.toml +++ b/crates/db-seed/Cargo.toml @@ -4,29 +4,20 @@ version = "0.1.0" edition = "2021" [dependencies] -model = { path = "../shared/model", version = "0.1", features = ["db", "dummy"] } -config = { path = "../shared/config" } -database_manager = { path = "../actors/database_manager", features = ["dummy"] } -fs_manager = { path = "../actors/fs_manager", features = [] } - -bytes = { version = "1.1.0" } - actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } actix-web = { version = "4.0", features = [] } - -tokio = { version = "1.18.1", features = ["full"] } - -fake = { version = "2.4.3", features = ["derive", "chrono", "http"] } -rand = { version = "0.8.5" } - +bytes = { version = "1.1.0" } +config = { path = "../config" } +database_manager = { path = "../database_manager", features = ["dummy"] } dotenv = { version = "0.15", features = [] } - +fake = { version = "2.4.3", features = ["derive", "chrono", "http"] } +fs_manager = { path = "../fs_manager", features = [] } +human-panic = { version = "1.0.3" } +model = { path = "../model", version = "0.1", features = ["db", "dummy"] } +password-hash = { version = "0.4", features = ["alloc"] } +rand = { version = "0.8.5" } +thiserror = { version = "1.0.31" } +tokio = { version = "1.18.1", features = ["full"] } tracing = { version = "0.1.34" } tracing-subscriber = { version = "0.3.11" } - -password-hash = { version = "0.4", features = ["alloc"] } - -thiserror = { version = "1.0.31" } - -human-panic = { version = "1.0.3" } diff --git a/db-seed/src/accounts.rs b/crates/db-seed/src/accounts.rs similarity index 100% rename from db-seed/src/accounts.rs rename to crates/db-seed/src/accounts.rs diff --git a/db-seed/src/main.rs b/crates/db-seed/src/main.rs similarity index 100% rename from db-seed/src/main.rs rename to crates/db-seed/src/main.rs diff --git a/db-seed/src/photos.rs b/crates/db-seed/src/photos.rs similarity index 100% rename from db-seed/src/photos.rs rename to crates/db-seed/src/photos.rs diff --git a/db-seed/src/product_photos.rs b/crates/db-seed/src/product_photos.rs similarity index 100% rename from db-seed/src/product_photos.rs rename to crates/db-seed/src/product_photos.rs diff --git a/db-seed/src/products.rs b/crates/db-seed/src/products.rs similarity index 100% rename from db-seed/src/products.rs rename to crates/db-seed/src/products.rs diff --git a/actors/email_manager/Cargo.toml b/crates/email_manager/Cargo.toml similarity index 88% rename from actors/email_manager/Cargo.toml rename to crates/email_manager/Cargo.toml index e82aa72..100886e 100644 --- a/actors/email_manager/Cargo.toml +++ b/crates/email_manager/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } -model = { path = "../../shared/model" } +config = { path = "../config" } +model = { path = "../model" } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } sendgrid = { version = "0.17", features = ["async"] } diff --git a/actors/email_manager/assets/reset-password.html b/crates/email_manager/assets/reset-password.html similarity index 100% rename from actors/email_manager/assets/reset-password.html rename to crates/email_manager/assets/reset-password.html diff --git a/actors/email_manager/assets/style.css b/crates/email_manager/assets/style.css similarity index 100% rename from actors/email_manager/assets/style.css rename to crates/email_manager/assets/style.css diff --git a/actors/email_manager/assets/welcome.html b/crates/email_manager/assets/welcome.html similarity index 100% rename from actors/email_manager/assets/welcome.html rename to crates/email_manager/assets/welcome.html diff --git a/actors/email_manager/src/lib.rs b/crates/email_manager/src/lib.rs similarity index 100% rename from actors/email_manager/src/lib.rs rename to crates/email_manager/src/lib.rs diff --git a/actors/fs_manager/Cargo.toml b/crates/fs_manager/Cargo.toml similarity index 88% rename from actors/fs_manager/Cargo.toml rename to crates/fs_manager/Cargo.toml index c534c99..c086c12 100644 --- a/actors/fs_manager/Cargo.toml +++ b/crates/fs_manager/Cargo.toml @@ -9,9 +9,9 @@ actix-rt = { version = "2.7", features = [] } actix-web = { version = "4.0.1" } bytes = { version = "1.1.0" } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } +config = { path = "../config" } fibers_rpc = { version = "0.3.4", features = [] } -model = { path = "../../shared/model" } +model = { path = "../model" } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } serde = { version = "1.0", features = ["derive"] } diff --git a/actors/fs_manager/src/lib.rs b/crates/fs_manager/src/lib.rs similarity index 100% rename from actors/fs_manager/src/lib.rs rename to crates/fs_manager/src/lib.rs diff --git a/actors/lang_provider/Cargo.toml b/crates/lang_provider/Cargo.toml similarity index 82% rename from actors/lang_provider/Cargo.toml rename to crates/lang_provider/Cargo.toml index 2765547..542890d 100644 --- a/actors/lang_provider/Cargo.toml +++ b/crates/lang_provider/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" [dependencies] actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } -config = { path = "../../shared/config" } +config = { path = "../config" } fluent = { version = "0.16.0" } -model = { path = "../../shared/model" } +model = { path = "../model" } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } thiserror = { version = "1.0.31" } diff --git a/actors/lang_provider/locales/en/cart.ftl b/crates/lang_provider/locales/en/cart.ftl similarity index 100% rename from actors/lang_provider/locales/en/cart.ftl rename to crates/lang_provider/locales/en/cart.ftl diff --git a/actors/lang_provider/locales/pl/cart.ftl b/crates/lang_provider/locales/pl/cart.ftl similarity index 100% rename from actors/lang_provider/locales/pl/cart.ftl rename to crates/lang_provider/locales/pl/cart.ftl diff --git a/actors/lang_provider/src/lib.rs b/crates/lang_provider/src/lib.rs similarity index 100% rename from actors/lang_provider/src/lib.rs rename to crates/lang_provider/src/lib.rs diff --git a/shared/model/Cargo.toml b/crates/model/Cargo.toml similarity index 100% rename from shared/model/Cargo.toml rename to crates/model/Cargo.toml diff --git a/shared/model/src/api.rs b/crates/model/src/api.rs similarity index 100% rename from shared/model/src/api.rs rename to crates/model/src/api.rs diff --git a/shared/model/src/dummy.rs b/crates/model/src/dummy.rs similarity index 100% rename from shared/model/src/dummy.rs rename to crates/model/src/dummy.rs diff --git a/shared/model/src/encrypt.rs b/crates/model/src/encrypt.rs similarity index 100% rename from shared/model/src/encrypt.rs rename to crates/model/src/encrypt.rs diff --git a/shared/model/src/lib.rs b/crates/model/src/lib.rs similarity index 100% rename from shared/model/src/lib.rs rename to crates/model/src/lib.rs diff --git a/actors/order_manager/Cargo.toml b/crates/order_manager/Cargo.toml similarity index 86% rename from actors/order_manager/Cargo.toml rename to crates/order_manager/Cargo.toml index 834c62a..9071ce4 100644 --- a/actors/order_manager/Cargo.toml +++ b/crates/order_manager/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } +config = { path = "../config" } database_manager = { path = "../database_manager" } -model = { path = "../../shared/model" } +model = { path = "../model" } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } serde = { version = "1.0.137", features = ["derive"] } diff --git a/actors/order_manager/src/lib.rs b/crates/order_manager/src/lib.rs similarity index 100% rename from actors/order_manager/src/lib.rs rename to crates/order_manager/src/lib.rs diff --git a/actors/payment_manager/Cargo.toml b/crates/payment_manager/Cargo.toml similarity index 85% rename from actors/payment_manager/Cargo.toml rename to crates/payment_manager/Cargo.toml index eded3bc..5c6ae94 100644 --- a/actors/payment_manager/Cargo.toml +++ b/crates/payment_manager/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } +config = { path = "../config" } database_manager = { path = "../database_manager" } derive_more = { version = "0.99", features = [] } -model = { path = "../../shared/model" } +model = { path = "../model" } parking_lot = { version = "0.12", features = [] } pay_u = { version = '0.1', features = ["single-client"] } pretty_env_logger = { version = "0.4", features = [] } @@ -21,4 +21,4 @@ tracing = { version = "0.1.34" } uuid = { version = "0.8", features = ["serde"] } [dev-dependencies] -testx = { path = "../../shared/testx" } +testx = { path = "../testx" } diff --git a/actors/payment_manager/src/lib.rs b/crates/payment_manager/src/lib.rs similarity index 100% rename from actors/payment_manager/src/lib.rs rename to crates/payment_manager/src/lib.rs diff --git a/actors/payment_manager/src/pay_u_adapter/mod.rs b/crates/payment_manager/src/pay_u_adapter/mod.rs similarity index 100% rename from actors/payment_manager/src/pay_u_adapter/mod.rs rename to crates/payment_manager/src/pay_u_adapter/mod.rs diff --git a/actors/payment_manager/src/t_pay_adapter/mod.rs b/crates/payment_manager/src/t_pay_adapter/mod.rs similarity index 100% rename from actors/payment_manager/src/t_pay_adapter/mod.rs rename to crates/payment_manager/src/t_pay_adapter/mod.rs diff --git a/actors/search_manager/Cargo.toml b/crates/search_manager/Cargo.toml similarity index 88% rename from actors/search_manager/Cargo.toml rename to crates/search_manager/Cargo.toml index 1451ef3..0cde023 100644 --- a/actors/search_manager/Cargo.toml +++ b/crates/search_manager/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } +config = { path = "../config" } derive_more = { version = "0.99", features = [] } -model = { path = "../../shared/model" } +model = { path = "../model" } parking_lot = { version = "0.12", features = [] } pretty_env_logger = { version = "0.4", features = [] } rumqttc = { version = "*" } diff --git a/actors/search_manager/src/lib.rs b/crates/search_manager/src/lib.rs similarity index 100% rename from actors/search_manager/src/lib.rs rename to crates/search_manager/src/lib.rs diff --git a/shared/testx/Cargo.toml b/crates/testx/Cargo.toml similarity index 100% rename from shared/testx/Cargo.toml rename to crates/testx/Cargo.toml diff --git a/shared/testx/src/lib.rs b/crates/testx/src/lib.rs similarity index 100% rename from shared/testx/src/lib.rs rename to crates/testx/src/lib.rs diff --git a/actors/token_manager/Cargo.toml b/crates/token_manager/Cargo.toml similarity index 90% rename from actors/token_manager/Cargo.toml rename to crates/token_manager/Cargo.toml index 087ce29..7a18771 100644 --- a/actors/token_manager/Cargo.toml +++ b/crates/token_manager/Cargo.toml @@ -8,14 +8,14 @@ actix = { version = "0.13", features = [] } actix-rt = { version = "2.7", features = [] } argon2 = { version = "0.4", features = ["parallel", "password-hash"] } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../../shared/config" } +config = { path = "../config" } database_manager = { path = "../database_manager" } derive_more = { version = "0.99", features = [] } futures = { version = "0.3", features = [] } futures-util = { version = "0.3", features = [] } hmac = { version = "0.12", features = [] } jwt = { version = "0.16", features = [] } -model = { path = "../../shared/model" } +model = { path = "../model" } parking_lot = { version = "0.12", features = [] } password-hash = { version = "0.4", features = ["alloc"] } pretty_env_logger = { version = "0.4", features = [] } @@ -29,4 +29,4 @@ tracing = { version = "0.1.34" } uuid = { version = "1.2.1", features = ["serde"] } [dev-dependencies] -testx = { path = "../../shared/testx" } +testx = { path = "../testx" } diff --git a/actors/token_manager/src/lib.rs b/crates/token_manager/src/lib.rs similarity index 100% rename from actors/token_manager/src/lib.rs rename to crates/token_manager/src/lib.rs diff --git a/web/Cargo.toml b/crates/web/Cargo.toml similarity index 93% rename from web/Cargo.toml rename to crates/web/Cargo.toml index 6827077..a65e68f 100644 --- a/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -11,7 +11,7 @@ chrono = { version = "*", features = ["wasm-bindgen", "wasmbind"] } gloo-timers = { version = "*", features = ["futures"] } indexmap = { version = "1", default-features = false, features = ["serde-1", "std"] } js-sys = { version = "0.3.57", features = [] } -model = { path = "../shared/model", features = ["dummy"] } +model = { path = "../model", features = ["dummy"] } rusty-money = { version = "0.4.1", features = ["iso"] } seed = { version = "0.9.1", features = [] } serde = { version = "1.0.137", features = ["derive"] } diff --git a/web/Trunk.toml b/crates/web/Trunk.toml similarity index 100% rename from web/Trunk.toml rename to crates/web/Trunk.toml diff --git a/web/assets/css/normalize.css b/crates/web/assets/css/normalize.css similarity index 100% rename from web/assets/css/normalize.css rename to crates/web/assets/css/normalize.css diff --git a/web/assets/css/skeleton.css b/crates/web/assets/css/skeleton.css similarity index 100% rename from web/assets/css/skeleton.css rename to crates/web/assets/css/skeleton.css diff --git a/web/assets/images/favicon.png b/crates/web/assets/images/favicon.png similarity index 100% rename from web/assets/images/favicon.png rename to crates/web/assets/images/favicon.png diff --git a/web/assets/index.html b/crates/web/assets/index.html similarity index 100% rename from web/assets/index.html rename to crates/web/assets/index.html diff --git a/web/assets/logo.png b/crates/web/assets/logo.png similarity index 100% rename from web/assets/logo.png rename to crates/web/assets/logo.png diff --git a/crates/web/dist/index.html b/crates/web/dist/index.html new file mode 100644 index 0000000..2acc3f5 --- /dev/null +++ b/crates/web/dist/index.html @@ -0,0 +1,45 @@ + + + + + Bazzar + + + + + + + + +
+
+ + + \ No newline at end of file diff --git a/crates/web/dist/logo-5d25fab096a85f61.png b/crates/web/dist/logo-5d25fab096a85f61.png new file mode 100644 index 0000000..8fef885 Binary files /dev/null and b/crates/web/dist/logo-5d25fab096a85f61.png differ diff --git a/crates/web/dist/tailwind-ad72042a0c3f7966.css b/crates/web/dist/tailwind-ad72042a0c3f7966.css new file mode 100644 index 0000000..1575df2 --- /dev/null +++ b/crates/web/dist/tailwind-ad72042a0c3f7966.css @@ -0,0 +1,2060 @@ +/* +! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* +Ensure the default browser behavior of the `hidden` attribute. +*/ + +[hidden] { + display: none; +} + +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; +} + +[multiple] { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +.translate-x-2\/4, .-translate-y-1\/2, .rotate-0, .skew-x-0, .skew-y-0, .scale-x-100, .scale-y-100, .transform, .hover\:scale-110 { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; +} + +.shadow-md, .shadow-xl, .shadow-sm, .shadow-lg, .shadow { + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; +} + +.ring-2, .ring, .ring-inset, .focus\:ring { + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; +} + +.filter { + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.form-checkbox,.form-radio { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +.form-radio { + border-radius: 100%; +} + +.form-checkbox:focus,.form-radio:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +.form-checkbox:checked,.form-radio:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +.form-radio:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +.form-checkbox:checked:hover,.form-checkbox:checked:focus,.form-radio:checked:hover,.form-radio:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +.debug-screens::before { + position: fixed; + z-index: 2147483647; + bottom: 0; + left: 0; + padding: .3333333em .5em; + font-size: 12px; + line-height: 1; + font-family: sans-serif; + background-color: #000; + color: #fff; + box-shadow: 0 0 0 1px #fff; + content: 'screen: _'; +} + +@media (min-width: 640px) { + .debug-screens::before { + content: 'screen: sm'; + } +} + +@media (min-width: 768px) { + .debug-screens::before { + content: 'screen: md'; + } +} + +@media (min-width: 1024px) { + .debug-screens::before { + content: 'screen: lg'; + } +} + +@media (min-width: 1280px) { + .debug-screens::before { + content: 'screen: xl'; + } +} + +@media (min-width: 1536px) { + .debug-screens::before { + content: 'screen: 2xl'; + } +} + +.visible { + visibility: visible; +} + +.invisible { + visibility: hidden; +} + +.static { + position: static; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: -webkit-sticky; + position: sticky; +} + +.top-0 { + top: 0px; +} + +.left-0 { + left: 0px; +} + +.right-0 { + right: 0px; +} + +.bottom-auto { + bottom: auto; +} + +.left-auto { + left: auto; +} + +.bottom-0 { + bottom: 0px; +} + +.z-30 { + z-index: 30; +} + +.z-10 { + z-index: 10; +} + +.m-8 { + margin: 2rem; +} + +.m-auto { + margin: auto; +} + +.m-4 { + margin: 1rem; +} + +.m-3 { + margin: 0.75rem; +} + +.m-2 { + margin: 0.5rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.-mx-4 { + margin-left: -1rem; + margin-right: -1rem; +} + +.-mx-2 { + margin-left: -0.5rem; + margin-right: -0.5rem; +} + +.my-6 { + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.-mx-3 { + margin-left: -0.75rem; + margin-right: -0.75rem; +} + +.-mx-1 { + margin-left: -0.25rem; + margin-right: -0.25rem; +} + +.mt-12 { + margin-top: 3rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mr-auto { + margin-right: auto; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-full { + height: 100%; +} + +.h-6 { + height: 1.5rem; +} + +.h-screen { + height: 100vh; +} + +.h-64 { + height: 16rem; +} + +.h-14 { + height: 3.5rem; +} + +.h-24 { + height: 6rem; +} + +.h-8 { + height: 2rem; +} + +.h-4 { + height: 1rem; +} + +.h-12 { + height: 3rem; +} + +.h-10 { + height: 2.5rem; +} + +.h-16 { + height: 4rem; +} + +.h-5 { + height: 1.25rem; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-full { + width: 100%; +} + +.w-6 { + width: 1.5rem; +} + +.w-64 { + width: 16rem; +} + +.w-14 { + width: 3.5rem; +} + +.w-8 { + width: 2rem; +} + +.w-4 { + width: 1rem; +} + +.w-12 { + width: 3rem; +} + +.w-32 { + width: 8rem; +} + +.w-20 { + width: 5rem; +} + +.w-16 { + width: 4rem; +} + +.w-1\/2 { + width: 50%; +} + +.w-5 { + width: 1.25rem; +} + +.min-w-0 { + min-width: 0px; +} + +.max-w-7xl { + max-width: 80rem; +} + +.max-w-xs { + max-width: 20rem; +} + +.flex-auto { + flex: 1 1 auto; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-grow { + flex-grow: 1; +} + +.translate-x-2\/4 { + --tw-translate-x: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-0 { + --tw-rotate: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.skew-x-0 { + --tw-skew-x: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.skew-y-0 { + --tw-skew-y: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-x-100 { + --tw-scale-x: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-y-100 { + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-pointer { + cursor: pointer; +} + +.appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.justify-evenly { + justify-content: space-evenly; +} + +.gap-4 { + gap: 1rem; +} + +.space-x-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.25rem * var(--tw-space-x-reverse)); + margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2rem * var(--tw-space-x-reverse)); + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-r { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.rounded-b { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.border { + border-width: 1px; +} + +.border-r { + border-right-width: 1px; +} + +.border-t-4 { + border-top-width: 4px; +} + +.border-t-0 { + border-top-width: 0px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-none { + border-style: none; +} + +.border-transparent { + border-color: transparent; +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} + +.border-teal-600 { + --tw-border-opacity: 1; + border-color: rgb(13 148 136 / var(--tw-border-opacity)); +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + +.bg-indigo-500 { + --tw-bg-opacity: 1; + background-color: rgb(99 102 241 / var(--tw-bg-opacity)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-indigo-700 { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + +.bg-pink-700 { + --tw-bg-opacity: 1; + background-color: rgb(190 24 93 / var(--tw-bg-opacity)); +} + +.bg-gray-500 { + --tw-bg-opacity: 1; + background-color: rgb(107 114 128 / var(--tw-bg-opacity)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.fill-white { + fill: #fff; +} + +.object-fill { + -o-object-fit: fill; + object-fit: fill; +} + +.p-8 { + padding: 2rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-2\.5 { + padding: 0.625rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pl-5 { + padding-left: 1.25rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pl-3 { + padding-left: 0.75rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pr-1 { + padding-right: 0.25rem; +} + +.pl-1 { + padding-left: 0.25rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.font-semibold { + font-weight: 600; +} + +.font-extrabold { + font-weight: 800; +} + +.font-medium { + font-weight: 500; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.uppercase { + text-transform: uppercase; +} + +.italic { + font-style: italic; +} + +.leading-normal { + line-height: 1.5; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-tight { + letter-spacing: -0.025em; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.text-sky-600 { + --tw-text-opacity: 1; + color: rgb(2 132 199 / var(--tw-text-opacity)); +} + +.text-sky-900 { + --tw-text-opacity: 1; + color: rgb(12 74 110 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-indigo-700 { + --tw-text-opacity: 1; + color: rgb(67 56 202 / var(--tw-text-opacity)); +} + +.text-indigo-600 { + --tw-text-opacity: 1; + color: rgb(79 70 229 / var(--tw-text-opacity)); +} + +.text-indigo-800 { + --tw-text-opacity: 1; + color: rgb(55 48 163 / var(--tw-text-opacity)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); +} + +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + +.text-yellow-600 { + --tw-text-opacity: 1; + color: rgb(202 138 4 / var(--tw-text-opacity)); +} + +.text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity)); +} + +.text-teal-700 { + --tw-text-opacity: 1; + color: rgb(15 118 110 / var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-green-700 { + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity)); +} + +.text-indigo-500 { + --tw-text-opacity: 1; + color: rgb(99 102 241 / var(--tw-text-opacity)); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.ring-2 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-inset { + --tw-ring-inset: inset; +} + +.ring-indigo-300 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity)); +} + +.ring-white { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity)); +} + +.ring-blue-800\/50 { + --tw-ring-color: rgb(30 64 175 / 0.5); +} + +.ring-indigo-800\/50 { + --tw-ring-color: rgb(55 48 163 / 0.5); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition-colors { + transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition { + transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.hover\:scale-110:hover { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:bg-blue-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); +} + +.hover\:bg-indigo-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.hover\:bg-red-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 202 202 / var(--tw-bg-opacity)); +} + +.hover\:bg-teal-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(15 118 110 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.hover\:bg-indigo-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.hover\:text-teal-100:hover { + --tw-text-opacity: 1; + color: rgb(204 251 241 / var(--tw-text-opacity)); +} + +.hover\:text-black:hover { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.hover\:text-red-800:hover { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity)); +} + +.hover\:underline:hover { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.focus\:border-indigo-400:focus { + --tw-border-opacity: 1; + border-color: rgb(129 140 248 / var(--tw-border-opacity)); +} + +.focus\:border-indigo-500:focus { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity)); +} + +.focus\:bg-indigo-600:focus { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + +.focus\:bg-indigo-700:focus { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + +.focus\:text-black:focus { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-indigo-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity)); +} + +.focus\:ring-opacity-40:focus { + --tw-ring-opacity: 0.4; +} + +.active\:outline-none:active { + outline: 2px solid transparent; + outline-offset: 2px; +} + +@media (min-width: 640px) { + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} + +@media (min-width: 768px) { + .md\:ml-4 { + margin-left: 1rem; + } + + .md\:flex { + display: flex; + } + + .md\:inline-flex { + display: inline-flex; + } + + .md\:table-cell { + display: table-cell; + } + + .md\:h-80 { + height: 20rem; + } + + .md\:h-32 { + height: 8rem; + } + + .md\:w-4\/5 { + width: 80%; + } + + .md\:w-7\/12 { + width: 58.333333%; + } + + .md\:w-5\/12 { + width: 41.666667%; + } + + .md\:flex-1 { + flex: 1 1 0%; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:justify-end { + justify-content: flex-end; + } + + .md\:border-none { + border-style: none; + } + + .md\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .md\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } +} + +@media (min-width: 1024px) { + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:mt-2 { + margin-top: 0.5rem; + } + + .lg\:inline { + display: inline; + } + + .lg\:flex { + display: flex; + } + + .lg\:hidden { + display: none; + } + + .lg\:w-4\/5 { + width: 80%; + } + + .lg\:w-1\/2 { + width: 50%; + } + + .lg\:max-w-md { + max-width: 28rem; + } + + .lg\:grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .lg\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .lg\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } + + .lg\:px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + .lg\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .lg\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .lg\:pl-0 { + padding-left: 0px; + } + + .lg\:pr-10 { + padding-right: 2.5rem; + } + + .lg\:text-right { + text-align: right; + } + + .lg\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .lg\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .lg\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } +} diff --git a/crates/web/dist/web-edbdf95f1008e472.js b/crates/web/dist/web-edbdf95f1008e472.js new file mode 100644 index 0000000..0ca9e72 --- /dev/null +++ b/crates/web/dist/web-edbdf95f1008e472.js @@ -0,0 +1,1224 @@ + +let wasm; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let WASM_VECTOR_LEN = 0; + +let cachedUint8Memory0 = new Uint8Array(); + +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +const cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = new Int32Array(); + +function getInt32Memory0() { + if (cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + + } else { + state.a = a; + } + } + }; + real.original = state; + + return real; +} +function __wbg_adapter_30(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hde3274e016c6ee35(arg0, arg1, arg2); +} + +function makeClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + try { + return f(state.a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b); + state.a = 0; + + } + } + }; + real.original = state; + + return real; +} +function __wbg_adapter_33(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hbc97b06f6d54221d(arg0, arg1, addHeapObject(arg2)); +} + +function __wbg_adapter_36(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h050a2aed0d7bcc1a(arg0, arg1, addHeapObject(arg2)); +} + +function __wbg_adapter_39(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h43eb1506e9568b34(arg0, arg1, addHeapObject(arg2)); +} + +function __wbg_adapter_42(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6fe6a729a0fd53f3(arg0, arg1); +} + +function __wbg_adapter_45(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc19c2cba038b390c(arg0, arg1, addHeapObject(arg2)); +} + +/** +*/ +export function start() { + wasm.start(); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} + +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} + +function notDefined(what) { return () => { throw new Error(`${what} is not defined`); }; } +function __wbg_adapter_299(arg0, arg1, arg2, arg3, arg4) { + const ret = wasm.wasm_bindgen__convert__closures__invoke3_mut__ha255020e3898d1dc(arg0, arg1, addHeapObject(arg2), arg3, addHeapObject(arg4)); + return ret !== 0; +} + +function __wbg_adapter_302(arg0, arg1, arg2, arg3, arg4) { + wasm.wasm_bindgen__convert__closures__invoke3_mut__hcacb26b3508c4435(arg0, arg1, addHeapObject(arg2), arg3, addHeapObject(arg4)); +} + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function getImports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = JSON.stringify(obj === undefined ? null : obj); + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { + const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_object = function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbindgen_is_string = function(arg0) { + const ret = typeof(getObject(arg0)) === 'string'; + return ret; + }; + imports.wbg.__wbg_crypto_e1d53a1d73fb10b8 = function(arg0) { + const ret = getObject(arg0).crypto; + return addHeapObject(ret); + }; + imports.wbg.__wbg_msCrypto_6e7d3e1f92610cbb = function(arg0) { + const ret = getObject(arg0).msCrypto; + return addHeapObject(ret); + }; + imports.wbg.__wbg_getRandomValues_805f1c3d65988a5a = function() { return handleError(function (arg0, arg1) { + getObject(arg0).getRandomValues(getObject(arg1)); + }, arguments) }; + imports.wbg.__wbg_randomFillSync_6894564c2c334c42 = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2)); + }, arguments) }; + imports.wbg.__wbg_require_78a3dcfbdba9cbce = function() { return handleError(function () { + const ret = module.require; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_process_038c26bf42b093f8 = function(arg0) { + const ret = getObject(arg0).process; + return addHeapObject(ret); + }; + imports.wbg.__wbg_versions_ab37218d2f0b24a8 = function(arg0) { + const ret = getObject(arg0).versions; + return addHeapObject(ret); + }; + imports.wbg.__wbg_node_080f4b19d15bc1fe = function(arg0) { + const ret = getObject(arg0).node; + return addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_Window_acc97ff9f5d2c7b4 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Window; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_document_3ead31dbcad65886 = function(arg0) { + const ret = getObject(arg0).document; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_location_8cc8ccf27e342c0a = function(arg0) { + const ret = getObject(arg0).location; + return addHeapObject(ret); + }; + imports.wbg.__wbg_history_2a104346a1208269 = function() { return handleError(function (arg0) { + const ret = getObject(arg0).history; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_navigator_d1dcf282b97e2495 = function(arg0) { + const ret = getObject(arg0).navigator; + return addHeapObject(ret); + }; + imports.wbg.__wbg_performance_de9825f9a8678574 = function(arg0) { + const ret = getObject(arg0).performance; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_localStorage_753b6d15a844c3dc = function() { return handleError(function (arg0) { + const ret = getObject(arg0).localStorage; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_cancelAnimationFrame_679ac3913d7f9b34 = function() { return handleError(function (arg0, arg1) { + getObject(arg0).cancelAnimationFrame(arg1); + }, arguments) }; + imports.wbg.__wbg_requestAnimationFrame_4181656476a7d86c = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).requestAnimationFrame(getObject(arg1)); + return ret; + }, arguments) }; + imports.wbg.__wbg_fetch_0fe04905cccfc2aa = function(arg0, arg1) { + const ret = getObject(arg0).fetch(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_activeElement_832e9d448ca74309 = function(arg0) { + const ret = getObject(arg0).activeElement; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_createElement_976dbb84fe1661b5 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_createElementNS_1561aca8ee3693c0 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + const ret = getObject(arg0).createElementNS(arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_createTextNode_300f845fab76642f = function(arg0, arg1, arg2) { + const ret = getObject(arg0).createTextNode(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_getElementById_3a708b83e4f034d7 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).getElementById(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_querySelector_3628dc2c3319e7e0 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_pushState_38917fb88b4add30 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5)); + }, arguments) }; + imports.wbg.__wbg_instanceof_Node_b1195878cdeab85c = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Node; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_nodeType_14c07508a0fc6d37 = function(arg0) { + const ret = getObject(arg0).nodeType; + return ret; + }; + imports.wbg.__wbg_childNodes_7345d62ab4ea541a = function(arg0) { + const ret = getObject(arg0).childNodes; + return addHeapObject(ret); + }; + imports.wbg.__wbg_firstChild_2598ba49fa5ee006 = function(arg0) { + const ret = getObject(arg0).firstChild; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_textContent_77bd294928962f93 = function(arg0, arg1) { + const ret = getObject(arg1).textContent; + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_settextContent_538ceb17614272d8 = function(arg0, arg1, arg2) { + getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_appendChild_e513ef0e5098dfdd = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).appendChild(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_insertBefore_9f2d2defb9471006 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_removeChild_6751e9ca5d9aaf00 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).removeChild(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_replaceChild_4793d6269c04dd25 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).replaceChild(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_href_bbb11e0e61ea410e = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).href; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbg_getItem_845e475f85f593e4 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3)); + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }, arguments) }; + imports.wbg.__wbg_removeItem_9da69ede4eea3326 = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).removeItem(getStringFromWasm0(arg1, arg2)); + }, arguments) }; + imports.wbg.__wbg_setItem_9c469d634d0c321c = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { + const ret = new Headers(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_append_de37df908812970d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbg_instanceof_HtmlDataElement_22f1a23e556ad929 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLDataElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_d9c5ce821580eb1b = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_instanceof_HtmlProgressElement_f13bd076b66c526e = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLProgressElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_4559aec5dd435520 = function(arg0, arg1) { + getObject(arg0).value = arg1; + }; + imports.wbg.__wbg_status_c4ef3dd591e63435 = function(arg0) { + const ret = getObject(arg0).status; + return ret; + }; + imports.wbg.__wbg_statusText_7f6b7d97e47933bd = function(arg0, arg1) { + const ret = getObject(arg1).statusText; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_json_eb16b12f372e850c = function() { return handleError(function (arg0) { + const ret = getObject(arg0).json(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_text_1169d752cc697903 = function() { return handleError(function (arg0) { + const ret = getObject(arg0).text(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_instanceof_HtmlMenuItemElement_b156ddf64aa3302c = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLMenuItemElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setchecked_0b660f26d0a14cc1 = function(arg0, arg1) { + getObject(arg0).checked = arg1 !== 0; + }; + imports.wbg.__wbg_instanceof_HtmlOutputElement_fb2a300379e9e005 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLOutputElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_6a4b3a1cb0f76303 = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_new_ca4d3a3eca340210 = function() { return handleError(function () { + const ret = new URLSearchParams(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_append_cfc9c4de4429ccc1 = function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }; + imports.wbg.__wbg_instanceof_HtmlMeterElement_db13c38e5f2ffdd6 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLMeterElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_48084fca2143db20 = function(arg0, arg1) { + getObject(arg0).value = arg1; + }; + imports.wbg.__wbg_href_9b462d09b5f8b378 = function(arg0, arg1) { + const ret = getObject(arg1).href; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_pathname_78a642e573bf8169 = function(arg0, arg1) { + const ret = getObject(arg1).pathname; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_setsearch_40007c2a91333011 = function(arg0, arg1, arg2) { + getObject(arg0).search = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_searchParams_8f54380784e8678c = function(arg0) { + const ret = getObject(arg0).searchParams; + return addHeapObject(ret); + }; + imports.wbg.__wbg_hash_5ca9e2d439e2b3e1 = function(arg0, arg1) { + const ret = getObject(arg1).hash; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_sethash_d35570df091aa47e = function(arg0, arg1, arg2) { + getObject(arg0).hash = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_newwithbase_41b4a8c94dd8c467 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = new URL(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_instanceof_Element_33bd126d58f2021b = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Element; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_namespaceURI_e19c7be2c60e5b5c = function(arg0, arg1) { + const ret = getObject(arg1).namespaceURI; + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_tagName_50571f9480ac166a = function(arg0, arg1) { + const ret = getObject(arg1).tagName; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_closest_f03bae0dfb668e4e = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).closest(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_getAttribute_3a1f0fb396184372 = function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg1).getAttribute(getStringFromWasm0(arg2, arg3)); + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_getAttributeNames_d627981a5ccb4998 = function(arg0) { + const ret = getObject(arg0).getAttributeNames(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_hasAttribute_a9fb6bc740fe4146 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).hasAttribute(getStringFromWasm0(arg1, arg2)); + return ret; + }; + imports.wbg.__wbg_removeAttribute_beaed7727852af78 = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).removeAttribute(getStringFromWasm0(arg1, arg2)); + }, arguments) }; + imports.wbg.__wbg_setAttribute_d8436c14a59ab1af = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbg_instanceof_HtmlButtonElement_173e1d7a3882bdf2 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLButtonElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_38a29a98ce689a32 = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_instanceof_HtmlTextAreaElement_a091a90ac155d1ab = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLTextAreaElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_value_ccb32485ee1b3928 = function(arg0, arg1) { + const ret = getObject(arg1).value; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_setvalue_df64bc6794c098f2 = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_instanceof_HtmlElement_eff00d16af7bd6e7 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_focus_adfe4cc61e2c09bc = function() { return handleError(function (arg0) { + getObject(arg0).focus(); + }, arguments) }; + imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { + const ret = getObject(arg0).signal; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () { + const ret = new AbortController(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { + getObject(arg0).abort(); + }; + imports.wbg.__wbg_instanceof_HtmlInputElement_970e4026de0fccff = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLInputElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setchecked_f1e1f3e62cdca8e7 = function(arg0, arg1) { + getObject(arg0).checked = arg1 !== 0; + }; + imports.wbg.__wbg_type_6ce8af5475dcc48f = function(arg0, arg1) { + const ret = getObject(arg1).type; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_value_b2a620d34c663701 = function(arg0, arg1) { + const ret = getObject(arg1).value; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_setvalue_e5b519cca37d82a7 = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_selectionStart_3beaae3c5a2acda9 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).selectionStart; + getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); + }, arguments) }; + imports.wbg.__wbg_setselectionStart_505063491b8c34ac = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).selectionStart = arg1 === 0 ? undefined : arg2 >>> 0; + }, arguments) }; + imports.wbg.__wbg_selectionEnd_63ee35d5c4f0c791 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).selectionEnd; + getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); + }, arguments) }; + imports.wbg.__wbg_setselectionEnd_9fd2014aeceed3c4 = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).selectionEnd = arg1 === 0 ? undefined : arg2 >>> 0; + }, arguments) }; + imports.wbg.__wbg_instanceof_HtmlLiElement_cb29c3f068803cf5 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLLIElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_921d74730c6d0890 = function(arg0, arg1) { + getObject(arg0).value = arg1; + }; + imports.wbg.__wbg_instanceof_HtmlOptionElement_136a693b46a2ef54 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLOptionElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_a5cddb62e09e1ceb = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_now_8172cd917e5eda6b = function(arg0) { + const ret = getObject(arg0).now(); + return ret; + }; + imports.wbg.__wbg_error_ef9a0be47931175f = function(arg0) { + console.error(getObject(arg0)); + }; + imports.wbg.__wbg_log_4b5638ad60bdc54a = function(arg0) { + console.log(getObject(arg0)); + }; + imports.wbg.__wbg_addEventListener_cbe4c6f619b032f3 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); + }, arguments) }; + imports.wbg.__wbg_removeEventListener_dd20475efce70084 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); + }, arguments) }; + imports.wbg.__wbg_length_4b03cbe342879df8 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_get_eff2c5e76f778292 = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_Event_1009dd203d9055ee = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Event; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_target_bf704b7db7ad1387 = function(arg0) { + const ret = getObject(arg0).target; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_preventDefault_3209279b490de583 = function(arg0) { + getObject(arg0).preventDefault(); + }; + imports.wbg.__wbg_stopPropagation_eca3af16f2d02a91 = function(arg0) { + getObject(arg0).stopPropagation(); + }; + imports.wbg.__wbg_languages_1fd4c718eaeb903a = function(arg0) { + const ret = getObject(arg0).languages; + return addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_HtmlParamElement_530756bfa9838820 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLParamElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_6f5890fb930bda58 = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_instanceof_HtmlSelectElement_e8421685c2eaa299 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLSelectElement; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_setvalue_511e4a973ca603a8 = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_instanceof_KeyboardEvent_b7b8e59c000a4389 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof KeyboardEvent; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_charCode_b0f31612a52c2bff = function(arg0) { + const ret = getObject(arg0).charCode; + return ret; + }; + imports.wbg.__wbg_shiftKey_908ae224b8722a41 = function(arg0) { + const ret = getObject(arg0).shiftKey; + return ret; + }; + imports.wbg.__wbg_instanceof_PopStateEvent_9cbfe00f691c024b = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof PopStateEvent; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_state_8090cb876d9a02fe = function(arg0) { + const ret = getObject(arg0).state; + return addHeapObject(ret); + }; + imports.wbg.__wbg_setTimeout_02c3975efb677088 = function() { return handleError(function (arg0, arg1) { + const ret = setTimeout(getObject(arg0), arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_setInterval_02955d9da658654f = function() { return handleError(function (arg0, arg1) { + const ret = setInterval(getObject(arg0), arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_clearTimeout_5b4145302d77e5f3 = typeof clearTimeout == 'function' ? clearTimeout : notDefined('clearTimeout'); + imports.wbg.__wbg_clearInterval_ecd7aa9e55e617e9 = typeof clearInterval == 'function' ? clearInterval : notDefined('clearInterval'); + imports.wbg.__wbg_decodeURIComponent_0183f5f5ec5752fc = function() { return handleError(function (arg0, arg1) { + const ret = decodeURIComponent(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_encodeURIComponent_e7f444348deb4645 = function(arg0, arg1) { + const ret = encodeURIComponent(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_get_57245cc7d7c7619d = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_from_7ce3cb27cb258569 = function(arg0) { + const ret = Array.from(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_find_fcd1f16a36fc9934 = function(arg0, arg1, arg2) { + try { + var state0 = {a: arg1, b: arg2}; + var cb0 = (arg0, arg1, arg2) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_299(a, state0.b, arg0, arg1, arg2); + } finally { + state0.a = a; + } + }; + const ret = getObject(arg0).find(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_forEach_ce1177df15902e0c = function(arg0, arg1, arg2) { + try { + var state0 = {a: arg1, b: arg2}; + var cb0 = (arg0, arg1, arg2) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_302(a, state0.b, arg0, arg1, arg2); + } finally { + state0.a = a; + } + }; + getObject(arg0).forEach(cb0); + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_length_6e3bbe7c8bd4dbd8 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_getTime_cb82adb2556ed13e = function(arg0) { + const ret = getObject(arg0).getTime(); + return ret; + }; + imports.wbg.__wbg_getTimezoneOffset_89bd4275e1ca8341 = function(arg0) { + const ret = getObject(arg0).getTimezoneOffset(); + return ret; + }; + imports.wbg.__wbg_new0_a57059d72c5b7aee = function() { + const ret = new Date(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_is_40a66842732708e7 = function(arg0, arg1) { + const ret = Object.is(getObject(arg0), getObject(arg1)); + return ret; + }; + imports.wbg.__wbg_new_0b9bfdd97583284e = function() { + const ret = new Object(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_toString_7be108a12ef03bc2 = function(arg0) { + const ret = getObject(arg0).toString(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) { + const ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) { + const ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) { + const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () { + const ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () { + const ret = self.self; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_window_f2557cc78490aceb = function() { return handleError(function () { + const ret = window.window; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () { + const ret = global.global; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_newwithlength_f5933855e4f48a19 = function(arg0) { + const ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_subarray_58ad4efbb5bcb886 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_length_9e1ae1900cb0fbd5 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_set_83db9690f9353e79 = function(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); + }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_bf3f89b92d5a34bf = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments) }; + imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } + }; + imports.wbg.__wbg_new_abda76e883ba8a5f = function() { + const ret = new Error(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper4836 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 159, __wbg_adapter_30); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper4838 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 157, __wbg_adapter_33); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper4840 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 161, __wbg_adapter_36); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper4842 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 155, __wbg_adapter_39); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper22219 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1580, __wbg_adapter_42); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper22517 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1594, __wbg_adapter_45); + return addHeapObject(ret); + }; + + return imports; +} + +function initMemory(imports, maybe_memory) { + +} + +function finalizeInit(instance, module) { + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + cachedInt32Memory0 = new Int32Array(); + cachedUint8Memory0 = new Uint8Array(); + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + const imports = getImports(); + + initMemory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return finalizeInit(instance, module); +} + +async function init(input) { + if (typeof input === 'undefined') { + input = new URL('web-edbdf95f1008e472_bg.wasm', import.meta.url); + } + const imports = getImports(); + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + initMemory(imports); + + const { instance, module } = await load(await input, imports); + + return finalizeInit(instance, module); +} + +export { initSync } +export default init; diff --git a/crates/web/dist/web-edbdf95f1008e472_bg.wasm b/crates/web/dist/web-edbdf95f1008e472_bg.wasm new file mode 100644 index 0000000..b941788 Binary files /dev/null and b/crates/web/dist/web-edbdf95f1008e472_bg.wasm differ diff --git a/web/index.html b/crates/web/index.html similarity index 100% rename from web/index.html rename to crates/web/index.html diff --git a/web/package.json b/crates/web/package.json similarity index 100% rename from web/package.json rename to crates/web/package.json diff --git a/web/src/api.rs b/crates/web/src/api.rs similarity index 100% rename from web/src/api.rs rename to crates/web/src/api.rs diff --git a/web/src/api/admin.rs b/crates/web/src/api/admin.rs similarity index 100% rename from web/src/api/admin.rs rename to crates/web/src/api/admin.rs diff --git a/web/src/api/public.rs b/crates/web/src/api/public.rs similarity index 100% rename from web/src/api/public.rs rename to crates/web/src/api/public.rs diff --git a/web/src/debug/mod.rs b/crates/web/src/debug/mod.rs similarity index 100% rename from web/src/debug/mod.rs rename to crates/web/src/debug/mod.rs diff --git a/web/src/i18n.rs b/crates/web/src/i18n.rs similarity index 100% rename from web/src/i18n.rs rename to crates/web/src/i18n.rs diff --git a/web/src/i18n/pl.rs b/crates/web/src/i18n/pl.rs similarity index 100% rename from web/src/i18n/pl.rs rename to crates/web/src/i18n/pl.rs diff --git a/web/src/input.css b/crates/web/src/input.css similarity index 100% rename from web/src/input.css rename to crates/web/src/input.css diff --git a/web/src/lib.rs b/crates/web/src/lib.rs similarity index 100% rename from web/src/lib.rs rename to crates/web/src/lib.rs diff --git a/web/src/model.rs b/crates/web/src/model.rs similarity index 100% rename from web/src/model.rs rename to crates/web/src/model.rs diff --git a/web/src/pages.rs b/crates/web/src/pages.rs similarity index 100% rename from web/src/pages.rs rename to crates/web/src/pages.rs diff --git a/web/src/pages/admin.rs b/crates/web/src/pages/admin.rs similarity index 100% rename from web/src/pages/admin.rs rename to crates/web/src/pages/admin.rs diff --git a/web/src/pages/admin/landing.rs b/crates/web/src/pages/admin/landing.rs similarity index 100% rename from web/src/pages/admin/landing.rs rename to crates/web/src/pages/admin/landing.rs diff --git a/web/src/pages/public.rs b/crates/web/src/pages/public.rs similarity index 100% rename from web/src/pages/public.rs rename to crates/web/src/pages/public.rs diff --git a/web/src/pages/public/checkout.rs b/crates/web/src/pages/public/checkout.rs similarity index 100% rename from web/src/pages/public/checkout.rs rename to crates/web/src/pages/public/checkout.rs diff --git a/web/src/pages/public/listing.rs b/crates/web/src/pages/public/listing.rs similarity index 100% rename from web/src/pages/public/listing.rs rename to crates/web/src/pages/public/listing.rs diff --git a/web/src/pages/public/product.rs b/crates/web/src/pages/public/product.rs similarity index 100% rename from web/src/pages/public/product.rs rename to crates/web/src/pages/public/product.rs diff --git a/web/src/pages/public/shopping_cart.rs b/crates/web/src/pages/public/shopping_cart.rs similarity index 100% rename from web/src/pages/public/shopping_cart.rs rename to crates/web/src/pages/public/shopping_cart.rs diff --git a/web/src/pages/public/sign_in.rs b/crates/web/src/pages/public/sign_in.rs similarity index 100% rename from web/src/pages/public/sign_in.rs rename to crates/web/src/pages/public/sign_in.rs diff --git a/web/src/pages/public/sign_up.rs b/crates/web/src/pages/public/sign_up.rs similarity index 100% rename from web/src/pages/public/sign_up.rs rename to crates/web/src/pages/public/sign_up.rs diff --git a/web/src/session.rs b/crates/web/src/session.rs similarity index 100% rename from web/src/session.rs rename to crates/web/src/session.rs diff --git a/web/src/shared.rs b/crates/web/src/shared.rs similarity index 100% rename from web/src/shared.rs rename to crates/web/src/shared.rs diff --git a/web/src/shared/notification.rs b/crates/web/src/shared/notification.rs similarity index 100% rename from web/src/shared/notification.rs rename to crates/web/src/shared/notification.rs diff --git a/web/src/shared/view.rs b/crates/web/src/shared/view.rs similarity index 100% rename from web/src/shared/view.rs rename to crates/web/src/shared/view.rs diff --git a/web/src/shopping_cart.rs b/crates/web/src/shopping_cart.rs similarity index 100% rename from web/src/shopping_cart.rs rename to crates/web/src/shopping_cart.rs diff --git a/web/static/output.css b/crates/web/static/output.css similarity index 100% rename from web/static/output.css rename to crates/web/static/output.css diff --git a/web/static/reset-password.html b/crates/web/static/reset-password.html similarity index 100% rename from web/static/reset-password.html rename to crates/web/static/reset-password.html diff --git a/web/static/test-email.html b/crates/web/static/test-email.html similarity index 100% rename from web/static/test-email.html rename to crates/web/static/test-email.html diff --git a/web/tailwind.config.js b/crates/web/tailwind.config.js similarity index 100% rename from web/tailwind.config.js rename to crates/web/tailwind.config.js diff --git a/crates/web/tmp/tailwind.css b/crates/web/tmp/tailwind.css new file mode 100644 index 0000000..1575df2 --- /dev/null +++ b/crates/web/tmp/tailwind.css @@ -0,0 +1,2060 @@ +/* +! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* +Ensure the default browser behavior of the `hidden` attribute. +*/ + +[hidden] { + display: none; +} + +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; +} + +[multiple] { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +.translate-x-2\/4, .-translate-y-1\/2, .rotate-0, .skew-x-0, .skew-y-0, .scale-x-100, .scale-y-100, .transform, .hover\:scale-110 { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; +} + +.shadow-md, .shadow-xl, .shadow-sm, .shadow-lg, .shadow { + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; +} + +.ring-2, .ring, .ring-inset, .focus\:ring { + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; +} + +.filter { + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.form-checkbox,.form-radio { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +.form-radio { + border-radius: 100%; +} + +.form-checkbox:focus,.form-radio:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +.form-checkbox:checked,.form-radio:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +.form-radio:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +.form-checkbox:checked:hover,.form-checkbox:checked:focus,.form-radio:checked:hover,.form-radio:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +.debug-screens::before { + position: fixed; + z-index: 2147483647; + bottom: 0; + left: 0; + padding: .3333333em .5em; + font-size: 12px; + line-height: 1; + font-family: sans-serif; + background-color: #000; + color: #fff; + box-shadow: 0 0 0 1px #fff; + content: 'screen: _'; +} + +@media (min-width: 640px) { + .debug-screens::before { + content: 'screen: sm'; + } +} + +@media (min-width: 768px) { + .debug-screens::before { + content: 'screen: md'; + } +} + +@media (min-width: 1024px) { + .debug-screens::before { + content: 'screen: lg'; + } +} + +@media (min-width: 1280px) { + .debug-screens::before { + content: 'screen: xl'; + } +} + +@media (min-width: 1536px) { + .debug-screens::before { + content: 'screen: 2xl'; + } +} + +.visible { + visibility: visible; +} + +.invisible { + visibility: hidden; +} + +.static { + position: static; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: -webkit-sticky; + position: sticky; +} + +.top-0 { + top: 0px; +} + +.left-0 { + left: 0px; +} + +.right-0 { + right: 0px; +} + +.bottom-auto { + bottom: auto; +} + +.left-auto { + left: auto; +} + +.bottom-0 { + bottom: 0px; +} + +.z-30 { + z-index: 30; +} + +.z-10 { + z-index: 10; +} + +.m-8 { + margin: 2rem; +} + +.m-auto { + margin: auto; +} + +.m-4 { + margin: 1rem; +} + +.m-3 { + margin: 0.75rem; +} + +.m-2 { + margin: 0.5rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.-mx-4 { + margin-left: -1rem; + margin-right: -1rem; +} + +.-mx-2 { + margin-left: -0.5rem; + margin-right: -0.5rem; +} + +.my-6 { + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.-mx-3 { + margin-left: -0.75rem; + margin-right: -0.75rem; +} + +.-mx-1 { + margin-left: -0.25rem; + margin-right: -0.25rem; +} + +.mt-12 { + margin-top: 3rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mr-auto { + margin-right: auto; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-full { + height: 100%; +} + +.h-6 { + height: 1.5rem; +} + +.h-screen { + height: 100vh; +} + +.h-64 { + height: 16rem; +} + +.h-14 { + height: 3.5rem; +} + +.h-24 { + height: 6rem; +} + +.h-8 { + height: 2rem; +} + +.h-4 { + height: 1rem; +} + +.h-12 { + height: 3rem; +} + +.h-10 { + height: 2.5rem; +} + +.h-16 { + height: 4rem; +} + +.h-5 { + height: 1.25rem; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-full { + width: 100%; +} + +.w-6 { + width: 1.5rem; +} + +.w-64 { + width: 16rem; +} + +.w-14 { + width: 3.5rem; +} + +.w-8 { + width: 2rem; +} + +.w-4 { + width: 1rem; +} + +.w-12 { + width: 3rem; +} + +.w-32 { + width: 8rem; +} + +.w-20 { + width: 5rem; +} + +.w-16 { + width: 4rem; +} + +.w-1\/2 { + width: 50%; +} + +.w-5 { + width: 1.25rem; +} + +.min-w-0 { + min-width: 0px; +} + +.max-w-7xl { + max-width: 80rem; +} + +.max-w-xs { + max-width: 20rem; +} + +.flex-auto { + flex: 1 1 auto; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-grow { + flex-grow: 1; +} + +.translate-x-2\/4 { + --tw-translate-x: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-0 { + --tw-rotate: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.skew-x-0 { + --tw-skew-x: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.skew-y-0 { + --tw-skew-y: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-x-100 { + --tw-scale-x: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-y-100 { + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-pointer { + cursor: pointer; +} + +.appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.justify-evenly { + justify-content: space-evenly; +} + +.gap-4 { + gap: 1rem; +} + +.space-x-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.25rem * var(--tw-space-x-reverse)); + margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2rem * var(--tw-space-x-reverse)); + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-r { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.rounded-b { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.border { + border-width: 1px; +} + +.border-r { + border-right-width: 1px; +} + +.border-t-4 { + border-top-width: 4px; +} + +.border-t-0 { + border-top-width: 0px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-none { + border-style: none; +} + +.border-transparent { + border-color: transparent; +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} + +.border-teal-600 { + --tw-border-opacity: 1; + border-color: rgb(13 148 136 / var(--tw-border-opacity)); +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + +.bg-indigo-500 { + --tw-bg-opacity: 1; + background-color: rgb(99 102 241 / var(--tw-bg-opacity)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-indigo-700 { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + +.bg-pink-700 { + --tw-bg-opacity: 1; + background-color: rgb(190 24 93 / var(--tw-bg-opacity)); +} + +.bg-gray-500 { + --tw-bg-opacity: 1; + background-color: rgb(107 114 128 / var(--tw-bg-opacity)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.fill-white { + fill: #fff; +} + +.object-fill { + -o-object-fit: fill; + object-fit: fill; +} + +.p-8 { + padding: 2rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-2\.5 { + padding: 0.625rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pl-5 { + padding-left: 1.25rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pl-3 { + padding-left: 0.75rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pr-1 { + padding-right: 0.25rem; +} + +.pl-1 { + padding-left: 0.25rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.font-semibold { + font-weight: 600; +} + +.font-extrabold { + font-weight: 800; +} + +.font-medium { + font-weight: 500; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.uppercase { + text-transform: uppercase; +} + +.italic { + font-style: italic; +} + +.leading-normal { + line-height: 1.5; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-tight { + letter-spacing: -0.025em; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.text-sky-600 { + --tw-text-opacity: 1; + color: rgb(2 132 199 / var(--tw-text-opacity)); +} + +.text-sky-900 { + --tw-text-opacity: 1; + color: rgb(12 74 110 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-indigo-700 { + --tw-text-opacity: 1; + color: rgb(67 56 202 / var(--tw-text-opacity)); +} + +.text-indigo-600 { + --tw-text-opacity: 1; + color: rgb(79 70 229 / var(--tw-text-opacity)); +} + +.text-indigo-800 { + --tw-text-opacity: 1; + color: rgb(55 48 163 / var(--tw-text-opacity)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); +} + +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + +.text-yellow-600 { + --tw-text-opacity: 1; + color: rgb(202 138 4 / var(--tw-text-opacity)); +} + +.text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity)); +} + +.text-teal-700 { + --tw-text-opacity: 1; + color: rgb(15 118 110 / var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-green-700 { + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity)); +} + +.text-indigo-500 { + --tw-text-opacity: 1; + color: rgb(99 102 241 / var(--tw-text-opacity)); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.ring-2 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-inset { + --tw-ring-inset: inset; +} + +.ring-indigo-300 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity)); +} + +.ring-white { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity)); +} + +.ring-blue-800\/50 { + --tw-ring-color: rgb(30 64 175 / 0.5); +} + +.ring-indigo-800\/50 { + --tw-ring-color: rgb(55 48 163 / 0.5); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition-colors { + transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition { + transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.hover\:scale-110:hover { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:bg-blue-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); +} + +.hover\:bg-indigo-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.hover\:bg-red-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 202 202 / var(--tw-bg-opacity)); +} + +.hover\:bg-teal-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(15 118 110 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.hover\:bg-indigo-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.hover\:text-teal-100:hover { + --tw-text-opacity: 1; + color: rgb(204 251 241 / var(--tw-text-opacity)); +} + +.hover\:text-black:hover { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.hover\:text-red-800:hover { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity)); +} + +.hover\:underline:hover { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.focus\:border-indigo-400:focus { + --tw-border-opacity: 1; + border-color: rgb(129 140 248 / var(--tw-border-opacity)); +} + +.focus\:border-indigo-500:focus { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity)); +} + +.focus\:bg-indigo-600:focus { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + +.focus\:bg-indigo-700:focus { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity)); +} + +.focus\:text-black:focus { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-indigo-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity)); +} + +.focus\:ring-opacity-40:focus { + --tw-ring-opacity: 0.4; +} + +.active\:outline-none:active { + outline: 2px solid transparent; + outline-offset: 2px; +} + +@media (min-width: 640px) { + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} + +@media (min-width: 768px) { + .md\:ml-4 { + margin-left: 1rem; + } + + .md\:flex { + display: flex; + } + + .md\:inline-flex { + display: inline-flex; + } + + .md\:table-cell { + display: table-cell; + } + + .md\:h-80 { + height: 20rem; + } + + .md\:h-32 { + height: 8rem; + } + + .md\:w-4\/5 { + width: 80%; + } + + .md\:w-7\/12 { + width: 58.333333%; + } + + .md\:w-5\/12 { + width: 41.666667%; + } + + .md\:flex-1 { + flex: 1 1 0%; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:justify-end { + justify-content: flex-end; + } + + .md\:border-none { + border-style: none; + } + + .md\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .md\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } +} + +@media (min-width: 1024px) { + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:mt-2 { + margin-top: 0.5rem; + } + + .lg\:inline { + display: inline; + } + + .lg\:flex { + display: flex; + } + + .lg\:hidden { + display: none; + } + + .lg\:w-4\/5 { + width: 80%; + } + + .lg\:w-1\/2 { + width: 50%; + } + + .lg\:max-w-md { + max-width: 28rem; + } + + .lg\:grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .lg\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .lg\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } + + .lg\:px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + .lg\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .lg\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .lg\:pl-0 { + padding-left: 0px; + } + + .lg\:pr-10 { + padding-right: 2.5rem; + } + + .lg\:text-right { + text-align: right; + } + + .lg\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .lg\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .lg\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } +} diff --git a/web/yarn.lock b/crates/web/yarn.lock similarity index 100% rename from web/yarn.lock rename to crates/web/yarn.lock diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 0000000..b99104c --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env zsh + +source .env + +psql postgres postgres -c "CREATE DATABASE ${DATABASE_NAME}_accounts" || 0 +sqlx migrate run -D "${ACCOUNT_DATABASE_URL}" --source ./crates/account_manager/migrations + +psql postgres postgres -c "CREATE DATABASE ${DATABASE_NAME}_carts" || 0 +sqlx migrate run -D "${CART_DATABASE_URL}" --source ./crates/cart_manager/migrations diff --git a/shared/channels/src/lib.rs b/shared/channels/src/lib.rs deleted file mode 100644 index 965e69c..0000000 --- a/shared/channels/src/lib.rs +++ /dev/null @@ -1,171 +0,0 @@ -#![feature(structural_match)] - -#[derive(Clone)] -pub struct AsyncClient(pub rumqttc::AsyncClient); - -impl AsyncClient { - pub async fn publish, T: serde::Serialize>( - &self, - topic: Topic, - qos: rumqttc::QoS, - retain: bool, - t: T, - ) -> Result<(), rumqttc::ClientError> { - let v = bincode::serialize(&t).unwrap_or_default(); - let bytes = bytes::Bytes::copy_from_slice(&v); - self.0.publish_bytes(topic, qos, retain, bytes).await - } - - pub async fn publish_or_log, T: serde::Serialize>( - &self, - topic: Topic, - qos: rumqttc::QoS, - retain: bool, - t: T, - ) { - if let Err(e) = self.publish(topic, qos, retain, t).await { - tracing::error!("{}", e); - } - } -} - -pub mod account { - use model::{Email, Login, Password, Role}; - - #[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)] - pub enum Error { - #[error("mqtt payload has invalid create account data")] - InvalidCreateAccount, - #[error("mqtt payload has invalid account failure data")] - InvalidAccountFailure, - #[error("Account does not exists")] - Account, - #[error("Account does have any addresses")] - Addresses, - } - - pub static CLIENT_NAME: &str = "account-manager"; - - #[derive(Copy, Clone, Debug, PartialOrd, PartialEq, serde::Serialize, serde::Deserialize)] - pub enum Topic { - CreateAccount, - AccountCreated, - SignUpFailure, - } - - impl Into for Topic { - fn into(self) -> String { - String::from(self.to_str()) - } - } - - impl<'s> PartialEq<&'s str> for Topic { - fn eq(&self, other: &&'s str) -> bool { - self.to_str() == *other - } - } - - impl PartialEq for Topic { - fn eq(&self, other: &String) -> bool { - self.to_str() == other.as_str() - } - } - - impl Topic { - pub fn to_str(self) -> &'static str { - match self { - Topic::CreateAccount => "account/create", - Topic::AccountCreated => "account/created", - Topic::SignUpFailure => "account/failure", - } - } - } - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - pub struct CreateAccount { - pub email: Email, - pub login: Login, - pub password: Password, - pub role: Role, - } - - impl TryFrom for CreateAccount { - type Error = Error; - - fn try_from(value: bytes::Bytes) -> Result { - bincode::deserialize(value.as_ref()).map_err(|e| { - tracing::error!("{}", e); - Error::InvalidCreateAccount - }) - } - } - - #[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)] - pub enum AccountFailure { - #[error("Failed to hash password")] - FailedToHashPassword, - #[error("Failed to save account")] - SaveAccount, - #[error("Internal server error")] - InternalServerError, - } - - impl TryFrom for AccountFailure { - type Error = Error; - - fn try_from(value: bytes::Bytes) -> Result { - bincode::deserialize(value.as_ref()).map_err(|e| { - tracing::error!("{}", e); - Error::InvalidAccountFailure - }) - } - } - - #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] - pub struct MeResult { - pub account: Option, - pub addresses: Option>, - pub error: Option, - } - - #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] - pub struct RegisterResult { - pub account: Option, - pub error: Option, - } - - pub mod rpc { - use config::SharedAppConfig; - - #[tarpc::service] - pub trait Accounts { - /// Returns a greeting for name. - async fn me(account_id: model::AccountId) -> crate::account::MeResult; - - /// Creates new user account. - async fn register_account( - details: crate::account::CreateAccount, - ) -> crate::account::RegisterResult; - } - - pub async fn create_client(config: SharedAppConfig) -> AccountsClient { - use tarpc::client; - use tarpc::tokio_serde::formats::Json; - - let addr = { - let l = config.lock(); - (l.account_manager().bind.clone(), l.account_manager().port) - }; - - let transport = tarpc::serde_transport::tcp::connect(addr, Json::default); - - let client = AccountsClient::new( - client::Config::default(), - transport.await.expect("Failed to connect to server"), - ) - .spawn(); - - client - } - } -}