diff --git a/.env b/.env
index 86a9808..3317a1f 100644
--- a/.env
+++ b/.env
@@ -1,8 +1,12 @@
DATABASE_URL=postgres://postgres@localhost/bazzar
+
PASS_SALT=18CHwV7eGFAea16z+qMKZg
RUST_LOG=debug
SESSION_SECRET="NEPJs#8jjn8SK8GC7QEC^*P844UgsyEbQB8mRWXkT%3mPrwewZoc25MMby9H#R*w2KzaQgMkk#Pif$kxrLy*N5L!Ch%jxbWoa%gb"
JWT_SECRET="42^iFq&ZnQbUf!hwGWXd&CpyY6QQyJmkPU%esFCvne5&Ejcb3nJ4&GyHZp!MArZLf^9*5c6!!VgM$iZ8T%d#&bWTi&xbZk2S@4RN"
+SIGNATURE=David
+SERVICE_NAME="BaZZaR develop"
+
PGDATESTYLE=
SENDGRID_SECRET=SG.CUWRM-eoQfGJNqSU2bbwkg.NW5aBy5vZueCSOwIIyWUBqq5USChGiwAFrWzreBKvOU
@@ -13,7 +17,8 @@ PAYU_CLIENT_ID="145227"
PAYU_CLIENT_SECRET="12f071174cb7eb79d4aac5bc2f07563f"
PAYU_CLIENT_MERCHANT_ID=300746
-WEB_HOST=https://bazzar.ita-prog.pl
+#WEB_HOST=https://bazzar.ita-prog.pl
+WEB_HOST=0.0.0.0
FILES_PUBLIC_PATH=/files
FILES_LOCAL_PATH=./tmp
diff --git a/Cargo.lock b/Cargo.lock
index 50ae28d..db0e9bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -113,7 +113,6 @@ dependencies = [
"mime_guess",
"percent-encoding",
"pin-project-lite",
- "tokio-uring 0.2.0",
]
[[package]]
@@ -240,7 +239,6 @@ dependencies = [
"actix-macros",
"futures-core",
"tokio",
- "tokio-uring 0.3.0",
]
[[package]]
@@ -258,7 +256,6 @@ dependencies = [
"num_cpus",
"socket2",
"tokio",
- "tokio-uring 0.3.0",
"tracing",
]
@@ -1259,7 +1256,10 @@ dependencies = [
"model",
"pretty_env_logger",
"sendgrid",
+ "serde",
+ "serde_json",
"thiserror",
+ "tinytemplate",
"uuid",
]
@@ -1849,7 +1849,7 @@ dependencies = [
"hyper",
"rustls 0.20.4",
"tokio",
- "tokio-rustls 0.23.3",
+ "tokio-rustls 0.23.4",
]
[[package]]
@@ -1920,16 +1920,6 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "io-uring"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d75829ed9377bab6c90039fe47b9d84caceb4b5063266142e21bcce6550cda8"
-dependencies = [
- "bitflags",
- "libc",
-]
-
[[package]]
name = "ipnet"
version = "2.5.0"
@@ -2055,9 +2045,9 @@ dependencies = [
[[package]]
name = "local-channel"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6246c68cf195087205a0512559c97e15eaf95198bf0e206d662092cdcb03fe9f"
+checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c"
dependencies = [
"futures-core",
"futures-sink",
@@ -2067,9 +2057,9 @@ dependencies = [
[[package]]
name = "local-waker"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "902eb695eb0591864543cbfbf6d742510642a605a61fc5e97fe6ceb5a30ac4fb"
+checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1"
[[package]]
name = "lock_api"
@@ -2129,9 +2119,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
-version = "2.4.1"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
@@ -2208,25 +2198,14 @@ dependencies = [
[[package]]
name = "mio"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
+checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
dependencies = [
"libc",
"log",
- "miow",
- "ntapi",
"wasi 0.11.0+wasi-snapshot-preview1",
- "winapi",
-]
-
-[[package]]
-name = "miow"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
-dependencies = [
- "winapi",
+ "windows-sys",
]
[[package]]
@@ -2290,15 +2269,6 @@ dependencies = [
"minimal-lexical",
]
-[[package]]
-name = "ntapi"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
-dependencies = [
- "winapi",
-]
-
[[package]]
name = "num-bigint"
version = "0.3.3"
@@ -2322,9 +2292,9 @@ dependencies = [
[[package]]
name = "num-traits"
-version = "0.2.14"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
@@ -2341,9 +2311,9 @@ dependencies = [
[[package]]
name = "num_threads"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
@@ -2370,9 +2340,9 @@ dependencies = [
[[package]]
name = "object"
-version = "0.28.3"
+version = "0.28.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
+checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
dependencies = [
"memchr",
]
@@ -2397,18 +2367,30 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
-version = "0.10.38"
+version = "0.10.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
+checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
+ "openssl-macros",
"openssl-sys",
]
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "openssl-probe"
version = "0.1.5"
@@ -2417,9 +2399,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
-version = "0.9.72"
+version = "0.9.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
+checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
dependencies = [
"autocfg",
"cc",
@@ -2501,7 +2483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
dependencies = [
"lock_api",
- "parking_lot_core 0.9.2",
+ "parking_lot_core 0.9.3",
]
[[package]]
@@ -2520,9 +2502,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
-version = "0.9.2"
+version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
dependencies = [
"cfg-if",
"libc",
@@ -2754,9 +2736,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
-version = "1.0.37"
+version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
+checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
dependencies = [
"unicode-xid",
]
@@ -2923,7 +2905,7 @@ dependencies = [
"serde_urlencoded",
"tokio",
"tokio-native-tls",
- "tokio-rustls 0.23.3",
+ "tokio-rustls 0.23.4",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
@@ -2958,6 +2940,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "rust_decimal_macros"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c70be9367d4bc095d10b48d41b819d09ed4dafc528765a144d32ed1d530654"
+dependencies = [
+ "quote",
+ "rust_decimal",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.21"
@@ -2979,7 +2971,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
- "semver 1.0.7",
+ "semver 1.0.9",
]
[[package]]
@@ -3016,6 +3008,16 @@ dependencies = [
"base64",
]
+[[package]]
+name = "rusty-money"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683"
+dependencies = [
+ "rust_decimal",
+ "rust_decimal_macros",
+]
+
[[package]]
name = "ryu"
version = "1.0.9"
@@ -3041,12 +3043,6 @@ dependencies = [
"winapi",
]
-[[package]]
-name = "scoped-tls"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
-
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -3153,9 +3149,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.7"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4"
+checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd"
[[package]]
name = "semver-parser"
@@ -3337,12 +3333,12 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "socket2"
-version = "0.4.4"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+checksum = "ca642ba17f8b2995138b1d7711829c92e98c0a25ea019de790f4f09279c4e296"
dependencies = [
"libc",
- "winapi",
+ "windows-sys",
]
[[package]]
@@ -3709,6 +3705,16 @@ dependencies = [
"syn",
]
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "tinyvec"
version = "1.6.0"
@@ -3751,9 +3757,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.18.1"
+version = "1.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc"
+checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
dependencies = [
"bytes",
"libc",
@@ -3803,9 +3809,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
-version = "0.23.3"
+version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e"
+checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls 0.20.4",
"tokio",
@@ -3823,34 +3829,6 @@ dependencies = [
"tokio",
]
-[[package]]
-name = "tokio-uring"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "062a33613d97344c5054d9635b35beb14492b66d9aa4bdbf21ecde1682d256df"
-dependencies = [
- "bytes",
- "io-uring",
- "libc",
- "scoped-tls",
- "slab",
- "tokio",
-]
-
-[[package]]
-name = "tokio-uring"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3ad494f39874984d990ade7f6319dafbcd3301ff0b1841f8a55a1ebb3e742c8"
-dependencies = [
- "io-uring",
- "libc",
- "scoped-tls",
- "slab",
- "socket2",
- "tokio",
-]
-
[[package]]
name = "tokio-util"
version = "0.6.9"
@@ -4052,9 +4030,9 @@ checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "unicode_categories"
@@ -4270,6 +4248,8 @@ dependencies = [
"chrono",
"gloo-timers",
"indexmap",
+ "model",
+ "rusty-money",
"seed",
"serde",
"serde-wasm-bindgen",
@@ -4378,9 +4358,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
-version = "0.34.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
@@ -4391,33 +4371,33 @@ dependencies = [
[[package]]
name = "windows_aarch64_msvc"
-version = "0.34.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_i686_gnu"
-version = "0.34.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
-version = "0.34.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.34.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.34.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "winreg"
diff --git a/actors/database_manager/src/photos.rs b/actors/database_manager/src/photos.rs
index 1a4c080..741fe7b 100644
--- a/actors/database_manager/src/photos.rs
+++ b/actors/database_manager/src/photos.rs
@@ -20,7 +20,7 @@ where
{
sqlx::query_as(
r#"
-SELECT id, local_path, file_name
+SELECT id, local_path, file_name, unique_name
FROM photos
"#,
)
@@ -39,6 +39,7 @@ pub struct CreatePhoto {
pub local_path: model::LocalPath,
/// Only file name, this part should be also included in `local_path`
pub file_name: model::FileName,
+ pub unique_name: model::UniqueName,
}
crate::db_async_handler!(CreatePhoto, create_photo, model::Photo, inner_create_photo);
@@ -49,13 +50,14 @@ where
{
sqlx::query_as(
r#"
-INSERT INTO photos (file_name, local_path)
-VALUES ($1, $2)
-RETURNING id, local_path, file_name
+INSERT INTO photos (file_name, local_path, unique_name)
+VALUES ($1, $2, $3)
+RETURNING id, local_path, file_name, unique_name
"#,
)
.bind(msg.file_name)
.bind(msg.local_path)
+ .bind(msg.unique_name)
.fetch_one(pool)
.await
.map_err(|e| {
@@ -101,7 +103,7 @@ pub(crate) async fn photos_for_products(
) {
log::debug!("scoped product ids {:?}", ids);
let query: String = r#"
-SELECT photos.id, photos.local_path, photos.file_name, product_photos.product_id
+SELECT photos.id, photos.local_path, photos.file_name, product_photos.product_id, photos.unique_name
FROM photos
INNER JOIN product_photos
ON photos.id = product_photos.photo_id
diff --git a/actors/email_manager/Cargo.toml b/actors/email_manager/Cargo.toml
index 181cf23..eb851a0 100644
--- a/actors/email_manager/Cargo.toml
+++ b/actors/email_manager/Cargo.toml
@@ -19,3 +19,8 @@ chrono = { version = "0.4", features = ["serde"] }
log = { version = "0.4", features = [] }
pretty_env_logger = { version = "0.4", features = [] }
+
+tinytemplate = { version = "1.2.1" }
+
+serde = { version = "1.0", features = ["derive"] }
+serde_json = { version = "1.0", features = [] }
diff --git a/actors/email_manager/assets/reset-password.html b/actors/email_manager/assets/reset-password.html
new file mode 100644
index 0000000..c8cf040
--- /dev/null
+++ b/actors/email_manager/assets/reset-password.html
@@ -0,0 +1,34 @@
+
+
+
+
+ Reset password
+
+
+
+
+
+
+
+
Trouble signing in?
+
+
Resetting your password is easy.
+
+
Just press the button below and follow the instructions. We’ll have you up and running in no time.
+
+
+ Reset Password
+
+
+
+
If you did not make this request then please ignore this email.
+
+
+
+
+
diff --git a/actors/email_manager/assets/style.css b/actors/email_manager/assets/style.css
new file mode 100644
index 0000000..19e9f4a
--- /dev/null
+++ b/actors/email_manager/assets/style.css
@@ -0,0 +1,9 @@
+*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;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"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}
+sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}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}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}
+input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[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:0;padding-top:.5rem;padding-right:.75rem;padding-bottom:.5rem;padding-left:.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 .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:.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:0}[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}
+*,:before,:after{--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;--tw-scroll-snap-strictness:proximity;--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}.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}}.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'}}.mx-auto{margin-left:auto;margin-right:auto}.mt-12{margin-top:3rem}.mb-3{margin-bottom:.75rem}.mb-2{margin-bottom:.5rem}.inline-block{display:inline-block}.max-w-sm{max-width:24rem}.rounded-lg{border-radius:.5rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-transparent{border-color:transparent}.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))}.p-8{padding:2rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-semibold{font-weight:600}.leading-normal{line-height:1.5}.tracking-tight{letter-spacing:-.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))}.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-sky-600{--tw-shadow-color:#0284c7;--tw-shadow:var(--tw-shadow-colored)}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}
diff --git a/actors/email_manager/assets/welcome.html b/actors/email_manager/assets/welcome.html
new file mode 100644
index 0000000..6e6cc4f
--- /dev/null
+++ b/actors/email_manager/assets/welcome.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Welcome to { service_name }
+
+
+
+
+
+
+
+
Hi { login }
+
+
+ Welcome to {service_name} – we’re excited to have you on board and we’d love to say thank you on behalf
+ of our whole company for chosing us.
+
+
Take care,
+
{signature}
+
+
+
+
+
diff --git a/actors/email_manager/src/lib.rs b/actors/email_manager/src/lib.rs
index 5642fb7..d02cbfd 100644
--- a/actors/email_manager/src/lib.rs
+++ b/actors/email_manager/src/lib.rs
@@ -1,6 +1,7 @@
use std::sync::Arc;
use config::SharedAppConfig;
+use serde::Serialize;
#[macro_export]
macro_rules! mail_async_handler {
@@ -17,8 +18,13 @@ macro_rules! mail_async_handler {
};
}
+static STYLE: &str = include_str!("../assets/style.css");
+
#[derive(Debug, thiserror::Error)]
-pub enum Error {}
+pub enum Error {
+ #[error("Failed to render reset password template")]
+ ResetPassTemplate,
+}
pub type Result = std::result::Result;
@@ -31,6 +37,7 @@ pub struct EmailManager(Arc);
pub(crate) struct Inner {
config: SharedAppConfig,
send_grid: sendgrid::SGClient,
+ template: Arc>,
}
impl actix::Actor for EmailManager {
@@ -39,36 +46,143 @@ impl actix::Actor for EmailManager {
impl EmailManager {
pub fn build(config: SharedAppConfig) -> Result {
+ let template = {
+ use tinytemplate::*;
+ let mut t = TinyTemplate::new();
+ t.add_template(
+ "reset-password",
+ include_str!("../assets/reset-password.html"),
+ )
+ .expect("Failed to load e-mail template reset-password");
+ t.add_template("welcome", include_str!("../assets/welcome.html"))
+ .expect("Failed to load e-mail template welcome");
+ t
+ };
+
Ok(Self(Arc::new(Inner {
config: config.clone(),
send_grid: sendgrid::SGClient::new(config.lock().mail().sendgrid_secret()),
+ template: Arc::new(template),
})))
}
}
#[derive(actix::Message)]
-#[rtype(result = "Result")]
+#[rtype(result = "Result<()>")]
pub struct TestMail {
pub receiver: model::Email,
}
-mail_async_handler!(TestMail, test_mail, SendState);
+mail_async_handler!(TestMail, test_mail, ());
+
+pub(crate) async fn test_mail(msg: TestMail, inner: Arc) -> Result<()> {
+ welcome(
+ Welcome {
+ login: model::Login::new("Test User"),
+ email: msg.receiver,
+ },
+ inner,
+ )
+ .await
+}
+
+#[derive(actix::Message, Debug)]
+#[rtype(result = "Result<()>")]
+pub struct ResetPassword {
+ pub login: model::Login,
+ pub email: model::Email,
+ pub reset_token: model::ResetToken,
+}
+
+#[derive(Serialize)]
+struct ResetPasswordContext {
+ url: String,
+ style: &'static str,
+}
+
+mail_async_handler!(ResetPassword, reset_password, ());
+
+pub(crate) async fn reset_password(msg: ResetPassword, inner: Arc) -> Result<()> {
+ let host = { inner.config.lock().web().host() };
+ let context = ResetPasswordContext {
+ url: format!(
+ "{host}/reset-password/{reset_token}",
+ reset_token = msg.reset_token
+ ),
+ style: STYLE,
+ };
+ let html = inner
+ .template
+ .render("reset-password", &context)
+ .map_err(|e| {
+ log::error!("{e:?}");
+ Error::ResetPassTemplate
+ })?;
-pub(crate) async fn test_mail(msg: TestMail, inner: Arc) -> Result {
let status = inner
.send_grid
.send(
sendgrid::Mail::new()
- .add_to((msg.receiver.as_str(), "User").into())
+ .add_to((msg.email.as_str(), msg.login.as_str()).into())
.add_from(&inner.config.lock().mail().smtp_from())
- .add_subject("Test e-mail")
- .add_html("Test e-mail
")
+ .add_subject("Reset Password")
+ .add_html(html.as_str())
.build(),
)
.await;
log::debug!("{:?}", status);
- Ok(SendState {
- success: status.is_ok(),
- })
+
+ Ok(())
+}
+
+#[derive(actix::Message)]
+#[rtype(result = "Result<()>")]
+pub struct Welcome {
+ pub login: model::Login,
+ pub email: model::Email,
+}
+
+#[derive(Serialize)]
+struct WelcomeContext {
+ login: model::Login,
+ service_name: String,
+ signature: String,
+ style: &'static str,
+}
+
+mail_async_handler!(Welcome, welcome, ());
+
+pub(crate) async fn welcome(msg: Welcome, inner: Arc) -> Result<()> {
+ let (signature, service_name) = {
+ let l = inner.config.lock();
+ let w = l.web();
+ (w.signature(), w.service_name())
+ };
+ let context = WelcomeContext {
+ login: msg.login.clone(),
+ service_name,
+ signature,
+ style: STYLE,
+ };
+ let html = inner.template.render("welcome", &context).map_err(|e| {
+ log::error!("{e:?}");
+ Error::ResetPassTemplate
+ })?;
+
+ let status = inner
+ .send_grid
+ .send(
+ sendgrid::Mail::new()
+ .add_to((msg.email.as_str(), msg.login.as_str()).into())
+ .add_from(&inner.config.lock().mail().smtp_from())
+ .add_subject("Welcome")
+ .add_html(html.as_str())
+ .build(),
+ )
+ .await;
+
+ log::debug!("{:?}", status);
+
+ Ok(())
}
diff --git a/actors/fs_manager/src/lib.rs b/actors/fs_manager/src/lib.rs
index c44f66a..8e336e1 100644
--- a/actors/fs_manager/src/lib.rs
+++ b/actors/fs_manager/src/lib.rs
@@ -4,7 +4,7 @@ use std::ffi::OsStr;
use std::io::Write;
use config::SharedAppConfig;
-use model::{FileName, LocalPath};
+use model::{FileName, LocalPath, UniqueName};
#[macro_export]
macro_rules! fs_async_handler {
@@ -147,9 +147,10 @@ pub(crate) async fn remove_file(msg: RemoveFile, config: SharedAppConfig) -> Res
pub struct WriteResult {
/// Unique file name created with UUID and original file extension
- pub unique_name: FileName,
+ pub unique_name: UniqueName,
/// Path to file in local storage
pub local_path: LocalPath,
+ pub file_name: FileName,
}
#[derive(actix::Message)]
@@ -218,7 +219,8 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
log::debug!("File {:?} successfully written", unique_name);
Ok(WriteResult {
- unique_name: FileName::from(unique_name),
+ file_name: FileName::new(file_name),
+ unique_name: UniqueName::new(unique_name),
local_path: LocalPath::from(path.to_str().unwrap_or_default().to_string()),
})
}
diff --git a/api/Cargo.toml b/api/Cargo.toml
index 799f4af..172931d 100644
--- a/api/Cargo.toml
+++ b/api/Cargo.toml
@@ -27,7 +27,7 @@ actix-identity = { version = "0.4", features = [] }
actix-web-opentelemetry = { version = "0.12", features = [] }
actix-session = { version = "0.6", features = ["actix-redis", "redis-actor-session"] }
actix-redis = { version = "0.11", features = [] }
-actix-files = { version = "0.6", features = ["experimental-io-uring"] }
+actix-files = { version = "0.6", features = [] }
actix-multipart = { version = "0.4", features = [] }
gumdrop = { version = "0.8", features = [] }
diff --git a/api/src/main.rs b/api/src/main.rs
index b36f74e..265eff3 100644
--- a/api/src/main.rs
+++ b/api/src/main.rs
@@ -88,6 +88,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
.service({
let l = app_config.lock();
actix_files::Files::new(&l.files().public_path(), l.files().local_path())
+ .use_etag(true)
})
.default_service(actix_web::web::to(actix_web::HttpResponse::Ok))
})
@@ -181,20 +182,14 @@ async fn test_mailer(opts: TestMailerOpts) -> Result<()> {
let manager = email_manager::EmailManager::build(config)
.expect("Invalid email manager config")
.start();
- if manager
+ manager
.send(TestMail {
receiver: opts.receiver.expect("e-mail address is required"),
})
.await
.expect("Failed to execute actor")
- .expect("Failed to send email")
- .success
- {
- println!("Success!");
- } else {
- eprintln!("Failure!");
- std::process::exit(1);
- }
+ .expect("Failed to send email");
+ println!("Success!");
Ok(())
}
diff --git a/api/src/routes/admin/api_v1/products.rs b/api/src/routes/admin/api_v1/products.rs
index 98c85f9..1d57e23 100644
--- a/api/src/routes/admin/api_v1/products.rs
+++ b/api/src/routes/admin/api_v1/products.rs
@@ -2,6 +2,7 @@ use actix::Addr;
use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post, HttpResponse};
+use config::SharedAppConfig;
use database_manager::Database;
use model::{
api, Days, Price, ProductCategory, ProductId, ProductLongDesc, ProductName, ProductShortDesc,
@@ -18,9 +19,15 @@ use crate::{admin_send_db, routes};
async fn products(
session: Session,
db: Data>,
+ config: Data,
) -> routes::Result> {
session.require_admin()?;
+ let public_path = {
+ let l = config.lock();
+ l.files().public_path()
+ };
+
let db = db.into_inner();
let products = admin_send_db!(db, database_manager::AllProducts);
@@ -30,7 +37,7 @@ async fn products(
product_ids: products.iter().map(|p| p.id).collect()
}
);
- Ok(Json((products, photos).into()))
+ Ok(Json((products, photos, public_path).into()))
}
#[derive(Deserialize)]
diff --git a/api/src/routes/admin/api_v1/uploads.rs b/api/src/routes/admin/api_v1/uploads.rs
index a405f2c..d747ca3 100644
--- a/api/src/routes/admin/api_v1/uploads.rs
+++ b/api/src/routes/admin/api_v1/uploads.rs
@@ -64,9 +64,10 @@ async fn upload_product_image(
let write = async {
let fs_manager::WriteResult {
local_path,
- unique_name: file_name,
+ unique_name,
+ file_name,
} = match fs.send(msg).await {
- Ok(Ok(file_name)) => file_name,
+ Ok(Ok(res)) => res,
Ok(Err(e)) => {
log::error!("{e}");
return Err(UploadError::FileStreamBroken);
@@ -80,7 +81,8 @@ async fn upload_product_image(
db,
database_manager::CreatePhoto {
local_path,
- file_name
+ file_name,
+ unique_name
},
UploadError::DbSave
);
diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs
index 55241d9..b57978f 100644
--- a/api/src/routes/public/api_v1/unrestricted.rs
+++ b/api/src/routes/public/api_v1/unrestricted.rs
@@ -12,8 +12,15 @@ use crate::routes::{self, Result};
use crate::{public_send_db, Login, Password};
#[get("/products")]
-async fn products(db: Data>) -> Result> {
+async fn products(
+ db: Data>,
+ config: Data,
+) -> Result> {
let db = db.into_inner();
+ let public_path = {
+ let l = config.lock();
+ l.files().public_path()
+ };
let products: Vec = public_send_db!(owned, db, database_manager::AllProducts);
let photos: Vec = public_send_db!(
@@ -23,7 +30,7 @@ async fn products(db: Data>) -> Result> {
product_ids: products.iter().map(|p| p.id).collect()
}
);
- Ok(Json((products, photos).into()))
+ Ok(Json((products, photos, public_path).into()))
}
#[get("/stocks")]
diff --git a/migrations/20220509124526_add_unique_name_to_files.sql b/migrations/20220509124526_add_unique_name_to_files.sql
new file mode 100644
index 0000000..0a91d7f
--- /dev/null
+++ b/migrations/20220509124526_add_unique_name_to_files.sql
@@ -0,0 +1,2 @@
+ALTER TABLE photos
+ADD COLUMN unique_name TEXT NOT NULL DEFAULT gen_random_uuid() :: text UNIQUE;
diff --git a/migrations/20220509133117_update_photos.sql b/migrations/20220509133117_update_photos.sql
new file mode 100644
index 0000000..6f88921
--- /dev/null
+++ b/migrations/20220509133117_update_photos.sql
@@ -0,0 +1 @@
+update photos set unique_name = substring(local_path, 7);
diff --git a/shared/config/src/lib.rs b/shared/config/src/lib.rs
index 98aaa5e..cc5bbf5 100644
--- a/shared/config/src/lib.rs
+++ b/shared/config/src/lib.rs
@@ -46,7 +46,9 @@ pub struct PaymentConfig {
payu_client_secret: Option,
/// Create payu account and copy here merchant id
payu_client_merchant_id: Option,
+ currency: Option,
/// Allow customers to pay on site
+ #[serde(default)]
optional_payment: bool,
}
@@ -62,6 +64,7 @@ impl Example for PaymentConfig {
/// Create payu account and copy here merchant id
payu_client_merchant_id: Some(pay_u::MerchantPosId::from(0)),
/// Allow customers to pay on site
+ currency: None,
optional_payment: true,
}
}
@@ -104,6 +107,14 @@ impl PaymentConfig {
"payment config payu_client_merchant_id nor PAYU_CLIENT_MERCHANT_ID env was given",
)
}
+
+ pub fn currency(&self) -> String {
+ self.currency
+ .as_ref()
+ .cloned()
+ .or_else(|| std::env::var("CURRENCY").ok())
+ .unwrap_or_else(|| "PLN".into())
+ }
}
#[derive(Serialize, Deserialize, Default)]
@@ -119,17 +130,23 @@ pub struct WebConfig {
jwt_secret: Option,
bind: Option,
port: Option,
+ /// Name used in email signature
+ signature: Option,
+ /// Shop name
+ service_name: Option,
}
impl Example for WebConfig {
fn example() -> Self {
Self {
- host: Some(String::from("https://your.comain.com")),
- pass_salt: Some(String::from("Generate it with bazzar generate-hash")),
- session_secret: Some(String::from("100 characters long random string")),
- jwt_secret: Some(String::from("100 characters long random string")),
- bind: Some(String::from("0.0.0.0")),
+ host: Some("https://your.comain.com".into()),
+ pass_salt: Some("Generate it with bazzar generate-hash".into()),
+ session_secret: Some("100 characters long random string".into()),
+ jwt_secret: Some("100 characters long random string".into()),
+ bind: Some("0.0.0.0".into()),
port: Some(8080),
+ signature: Some("John Doe".into()),
+ service_name: Some("bazzar".into()),
}
}
}
@@ -198,6 +215,22 @@ impl WebConfig {
pub fn set_port(&mut self, port: u16) {
self.port = Some(port);
}
+
+ pub fn signature(&self) -> String {
+ self.signature
+ .as_ref()
+ .cloned()
+ .or_else(|| std::env::var("SIGNATURE").ok())
+ .expect("Web config signature nor SIGNATURE env was given")
+ }
+
+ pub fn service_name(&self) -> String {
+ self.service_name
+ .as_ref()
+ .cloned()
+ .or_else(|| std::env::var("SERVICE_NAME").ok())
+ .expect("Web config service_name nor SERVICE_NAME env was given")
+ }
}
#[derive(Serialize, Deserialize, Default)]
@@ -210,15 +243,11 @@ pub struct MailConfig {
impl Example for MailConfig {
fn example() -> Self {
Self {
- sendgrid_secret: Some(String::from(
- "Create sendgrid account and copy credentials here",
- )),
- sendgrid_api_key: Some(String::from(
- "Create sendgrid account and copy credentials here",
- )),
- smtp_from: Some(String::from(
- "Valid sendgrid authorized email address. Example: contact@example.com",
- )),
+ sendgrid_secret: Some("Create sendgrid account and copy credentials here".into()),
+ sendgrid_api_key: Some("Create sendgrid account and copy credentials here".into()),
+ smtp_from: Some(
+ "Valid sendgrid authorized email address. Example: contact@example.com".into(),
+ ),
}
}
}
@@ -257,7 +286,7 @@ pub struct DatabaseConfig {
impl Example for DatabaseConfig {
fn example() -> Self {
Self {
- url: Some(String::from("postgres://postgres@localhost/bazzar")),
+ url: Some("postgres://postgres@localhost/bazzar".into()),
}
}
}
@@ -282,6 +311,7 @@ pub struct SearchConfig {
sonic_search_pass: Option,
sonic_ingest_addr: Option,
sonic_ingest_pass: Option,
+ #[serde(default)]
search_active: bool,
}
@@ -300,10 +330,10 @@ impl Example for SearchConfig {
impl Default for SearchConfig {
fn default() -> Self {
Self {
- sonic_search_addr: Some(String::from("0.0.0.0:1491")),
- sonic_search_pass: Some(String::from("SecretPassword")),
- sonic_ingest_addr: Some(String::from("0.0.0.0:1491")),
- sonic_ingest_pass: Some(String::from("SecretPassword")),
+ sonic_search_addr: Some("0.0.0.0:1491".into()),
+ sonic_search_pass: Some("SecretPassword".into()),
+ sonic_ingest_addr: Some("0.0.0.0:1491".into()),
+ sonic_ingest_pass: Some("SecretPassword".into()),
search_active: true,
}
}
@@ -359,8 +389,8 @@ pub struct FilesConfig {
impl Example for FilesConfig {
fn example() -> Self {
Self {
- public_path: Some(String::from("/uploads")),
- local_path: Some(String::from("/var/local/bazzar")),
+ public_path: Some("/uploads".into()),
+ local_path: Some("/var/local/bazzar".into()),
}
}
}
@@ -368,8 +398,8 @@ impl Example for FilesConfig {
impl Default for FilesConfig {
fn default() -> Self {
Self {
- public_path: Some(String::from("/uploads")),
- local_path: Some(String::from("/var/local/bazzar")),
+ public_path: Some("/uploads".into()),
+ local_path: Some("/var/local/bazzar".into()),
}
}
}
@@ -380,7 +410,7 @@ impl FilesConfig {
.as_ref()
.cloned()
.or_else(|| std::env::var("FILES_PUBLIC_PATH").ok())
- .unwrap_or_else(|| String::from("/uploads"))
+ .unwrap_or_else(|| "/uploads".into())
}
pub fn local_path(&self) -> String {
@@ -388,17 +418,23 @@ impl FilesConfig {
.as_ref()
.cloned()
.or_else(|| std::env::var("FILES_LOCAL_PATH").ok())
- .unwrap_or_else(|| String::from("/var/local/bazzar"))
+ .unwrap_or_else(|| "/var/local/bazzar".into())
}
}
#[derive(Serialize, Deserialize)]
pub struct AppConfig {
+ #[serde(default)]
payment: PaymentConfig,
+ #[serde(default)]
web: WebConfig,
+ #[serde(default)]
mail: MailConfig,
+ #[serde(default)]
database: DatabaseConfig,
+ #[serde(default)]
search: SearchConfig,
+ #[serde(default)]
files: FilesConfig,
#[serde(skip)]
config_path: String,
@@ -514,6 +550,8 @@ SESSION_SECRET - 100 characters admin session encryption
JWT_SECRET - 100 characters user session encryption
BAZZAR_BIND - address to which server should be bind, typically 0.0.0.0
BAZZAR_PORT - port which server should use, typically 80
+SIGNATURE - Signature used in e-mails
+SERVICE_NAME - Shop name
SENDGRID_SECRET - e-mail sending service secret
SENDGRID_API_KEY - e-mail sending service api key
@@ -536,6 +574,6 @@ FILES_LOCAL_PATH - path where files are saved on server
}
pub fn save(config_path: &str, config: &mut AppConfig) {
- config.config_path = String::from(config_path);
+ config.config_path = config_path.into();
std::fs::write(config_path, toml::to_string_pretty(&config).unwrap()).unwrap();
}
diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs
index d03ba92..11651e7 100644
--- a/shared/model/src/api.rs
+++ b/shared/model/src/api.rs
@@ -1,8 +1,12 @@
-use serde::Serialize;
+use derive_more::Deref;
+#[cfg(feature = "dummy")]
+use fake::Fake;
+use serde::{Deserialize, Serialize};
use crate::ProductLinkedPhoto;
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug)]
#[serde(transparent)]
pub struct AccountOrders(pub Vec);
@@ -58,7 +62,8 @@ impl From<(crate::AccountOrder, Vec)> for AccountOrder {
}
}
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug)]
pub struct AccountOrder {
pub id: crate::AccountOrderId,
pub buyer_id: crate::AccountId,
@@ -67,7 +72,8 @@ pub struct AccountOrder {
pub items: Vec,
}
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug)]
pub struct ShoppingCartItem {
pub id: crate::ShoppingCartId,
pub product_id: crate::ProductId,
@@ -76,7 +82,8 @@ pub struct ShoppingCartItem {
pub quantity_unit: crate::QuantityUnit,
}
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug)]
pub struct ShoppingCart {
pub id: crate::ShoppingCartId,
pub buyer_id: crate::AccountId,
@@ -124,13 +131,17 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart
}
}
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug)]
pub struct Photo {
pub id: crate::PhotoId,
pub file_name: crate::FileName,
+ pub url: String,
+ pub unique_name: crate::UniqueName,
}
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug)]
pub struct Product {
pub id: crate::ProductId,
pub name: crate::ProductName,
@@ -142,7 +153,7 @@ pub struct Product {
pub photos: Vec,
}
-impl From<(crate::Product, &mut Vec)> for Product {
+impl<'path> From<(crate::Product, &mut Vec, &'path str)> for Product {
fn from(
(
crate::Product {
@@ -155,7 +166,8 @@ impl From<(crate::Product, &mut Vec)> for Product {
deliver_days_flag,
},
photos,
- ): (crate::Product, &mut Vec),
+ public_path,
+ ): (crate::Product, &mut Vec, &'path str),
) -> Self {
Self {
id,
@@ -173,23 +185,32 @@ impl From<(crate::Product, &mut Vec)> for Product {
local_path: _,
file_name,
product_id: _,
- }| Photo { id, file_name },
+ unique_name,
+ }| Photo {
+ id,
+ url: format!("{}/{}", public_path, unique_name.as_str()),
+ unique_name,
+ file_name,
+ },
)
.collect(),
}
}
}
-#[derive(Serialize, Debug)]
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[derive(Serialize, Deserialize, Debug, Deref)]
#[serde(transparent)]
-pub struct Products(Vec);
+pub struct Products(pub Vec);
-impl From<(Vec, Vec)> for Products {
- fn from((products, mut photos): (Vec, Vec)) -> Self {
+impl From<(Vec, Vec, String)> for Products {
+ fn from(
+ (products, mut photos, public_path): (Vec, Vec, String),
+ ) -> Self {
Self(
products
.into_iter()
- .map(|p| (p, &mut photos).into())
+ .map(|p| (p, &mut photos, public_path.as_str()).into())
.collect(),
)
}
diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs
index 4f47d4f..3c33300 100644
--- a/shared/model/src/lib.rs
+++ b/shared/model/src/lib.rs
@@ -12,6 +12,7 @@ use std::str::FromStr;
use derive_more::{Deref, Display, From};
#[cfg(feature = "dummy")]
use fake::Fake;
+#[cfg(feature = "dummy")]
use rand::Rng;
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
@@ -181,10 +182,16 @@ impl TryFrom for Quantity {
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
-#[derive(Deserialize, Serialize, Debug, Deref, From, Display)]
+#[derive(Deserialize, Serialize, Debug, Clone, Deref, From, Display)]
#[serde(transparent)]
pub struct Login(String);
+impl Login {
+ pub fn new>(s: S) -> Self {
+ Self(s.into())
+ }
+}
+
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Debug, Deref, From, Display)]
@@ -422,6 +429,18 @@ where
}
}
+#[cfg_attr(feature = "db", derive(sqlx::Type))]
+#[cfg_attr(feature = "db", sqlx(transparent))]
+#[derive(Serialize, Deserialize, Debug, Deref, Display)]
+#[serde(transparent)]
+pub struct ResetToken(String);
+
+impl ResetToken {
+ pub fn new>(s: S) -> Self {
+ Self(s.into())
+ }
+}
+
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Deref, From, Display)]
@@ -728,12 +747,36 @@ pub struct Token {
#[derive(Serialize, Deserialize, Debug, Deref, Display, From)]
pub struct TokenString(String);
+impl TokenString {
+ pub fn new>(s: S) -> Self {
+ Self(s.into())
+ }
+}
+
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Deref, Display, From)]
pub struct LocalPath(String);
+impl LocalPath {
+ pub fn new>(s: S) -> Self {
+ Self(s.into())
+ }
+}
+
+#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
+#[cfg_attr(feature = "db", derive(sqlx::Type))]
+#[cfg_attr(feature = "db", sqlx(transparent))]
+#[derive(Serialize, Deserialize, Debug, Deref, Display, From)]
+pub struct UniqueName(String);
+
+impl UniqueName {
+ pub fn new>(s: S) -> Self {
+ Self(s.into())
+ }
+}
+
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
@@ -765,6 +808,7 @@ pub struct Photo {
pub id: PhotoId,
pub local_path: LocalPath,
pub file_name: FileName,
+ pub unique_name: UniqueName,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
@@ -774,6 +818,7 @@ pub struct ProductLinkedPhoto {
pub id: PhotoId,
pub local_path: LocalPath,
pub file_name: FileName,
+ pub unique_name: UniqueName,
pub product_id: ProductId,
}
diff --git a/web/Cargo.toml b/web/Cargo.toml
index fd6c059..c001a36 100644
--- a/web/Cargo.toml
+++ b/web/Cargo.toml
@@ -7,6 +7,8 @@ edition = "2021"
crate-type = ["cdylib"]
[dependencies]
+model = { path = "../shared/model", features = ["dummy"] }
+
seed = { version = "0.9.1", features = [] }
chrono = { version = "*" }
@@ -21,6 +23,8 @@ web-sys = { version = "0.3.57", features = [] }
indexmap = { version = "1", features = ["serde-1"] }
+rusty-money = { version = "0.4.1", features = ["iso"] }
+
[profile.release]
lto = true
opt-level = 's'
diff --git a/web/Trunk.toml b/web/Trunk.toml
index 8d8f852..46a048d 100644
--- a/web/Trunk.toml
+++ b/web/Trunk.toml
@@ -1,2 +1,10 @@
[build]
target = "index.html"
+
+[[proxy]]
+rewrite = "/api/v1/"
+backend = "http://localhost:8080/api/v1"
+
+[[proxy]]
+rewrite = "/files"
+backend = "http://localhost:8080/files"
diff --git a/web/index.html b/web/index.html
index f910a77..9a59519 100644
--- a/web/index.html
+++ b/web/index.html
@@ -4,9 +4,7 @@
Bazzar
-
-
-
+
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..c2b3220
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,12 @@
+{
+ "dependencies": {
+ "@tailwindcss/aspect-ratio": "^0.4.0",
+ "@tailwindcss/forms": "^0.5.1",
+ "@tailwindcss/line-clamp": "^0.4.0",
+ "@tailwindcss/typography": "^0.5.2",
+ "autoprefixer": "^10.4.7",
+ "postcss": "^8.4.13",
+ "tailwindcss": "^3.0.24",
+ "tailwindcss-debug-screens": "^2.2.1"
+ }
+}
diff --git a/web/src/api.rs b/web/src/api.rs
new file mode 100644
index 0000000..86cbda1
--- /dev/null
+++ b/web/src/api.rs
@@ -0,0 +1 @@
+pub mod public;
diff --git a/web/src/api/public.rs b/web/src/api/public.rs
new file mode 100644
index 0000000..00a7895
--- /dev/null
+++ b/web/src/api/public.rs
@@ -0,0 +1,11 @@
+use seed::prelude::*;
+
+pub async fn fetch_products() -> fetch::Result {
+ Request::new("/api/v1/products")
+ .method(Method::Get)
+ .fetch()
+ .await?
+ .check_status()?
+ .json()
+ .await
+}
diff --git a/web/src/input.css b/web/src/input.css
new file mode 100644
index 0000000..a31e444
--- /dev/null
+++ b/web/src/input.css
@@ -0,0 +1,3 @@
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';
diff --git a/web/src/lib.rs b/web/src/lib.rs
index 31192d9..6e36b3e 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -1,18 +1,32 @@
+pub mod api;
mod model;
mod pages;
-use model::Model;
use seed::empty;
use seed::prelude::*;
+use crate::model::Model;
use crate::pages::{Msg, Page, PublicPage};
+macro_rules! fetch_page {
+ (public $model: expr, $page: ident, $ret: expr) => {{
+ let p = match &mut $model.page {
+ crate::pages::Page::Public(p) => p,
+ _ => return $ret,
+ };
+ match p {
+ crate::pages::PublicPage::$page(p) => p,
+ _ => return $ret,
+ }
+ }};
+}
+
fn init(url: Url, orders: &mut impl Orders) -> Model {
Model {
token: LocalStorage::get("auth-token").ok(),
page: Page::Public(PublicPage::Listing(pages::public::listing::init(
url,
- &mut orders.proxy(|msg| Msg::Public(pages::public::Msg::Listing(msg))),
+ &mut orders.proxy(proxy_public_listing),
))),
}
}
@@ -20,7 +34,10 @@ fn init(url: Url, orders: &mut impl Orders) -> Model {
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) {
match msg {
Msg::UrlChanged(subs::UrlChanged(url)) => model.page = Page::init(url, orders),
- Msg::Public(_) => {}
+ Msg::Public(pages::public::Msg::Listing(msg)) => {
+ let page = fetch_page!(public model, Listing, ());
+ pages::public::listing::update(msg, page, &mut orders.proxy(proxy_public_listing));
+ }
}
}
@@ -36,3 +53,7 @@ fn view(model: &Model) -> Node {
pub fn start() {
App::start("main", init, update, view);
}
+
+fn proxy_public_listing(msg: pages::public::listing::Msg) -> Msg {
+ Msg::Public(pages::public::Msg::Listing(msg))
+}
diff --git a/web/src/pages/public/listing.rs b/web/src/pages/public/listing.rs
index 99f18cb..f9c6b40 100644
--- a/web/src/pages/public/listing.rs
+++ b/web/src/pages/public/listing.rs
@@ -3,25 +3,93 @@ use seed::prelude::*;
use seed::*;
#[derive(Debug)]
-pub struct Model {}
-
-#[derive(Debug)]
-pub enum Msg {}
-
-pub fn init(url: Url, orders: &mut impl Orders) -> Model {
- Model {}
+pub struct Model {
+ pub products: Vec,
+ pub errors: Vec,
}
-pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) {}
+#[derive(Debug)]
+pub enum Msg {
+ FetchProducts,
+ ProductFetched(fetch::Result),
+}
+
+pub fn init(_url: Url, orders: &mut impl Orders) -> Model {
+ orders.send_msg(Msg::FetchProducts);
+ Model {
+ products: vec![],
+ errors: vec![],
+ }
+}
+
+pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) {
+ match msg {
+ Msg::FetchProducts => {
+ orders.skip().perform_cmd({
+ async { Msg::ProductFetched(crate::api::public::fetch_products().await) }
+ });
+ }
+ Msg::ProductFetched(Ok(products)) => {
+ model.products = products.0;
+ }
+ Msg::ProductFetched(Err(_e)) => {
+ model.errors.push("Failed to load products".into());
+ }
+ }
+}
pub fn view(model: &Model) -> Node {
+ let products = model.products.iter().map(product);
+
div![
- C!["container"],
+ C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
+ products
+ ]
+}
+
+fn product(product: &model::api::Product) -> Node {
+ use rusty_money::{iso, Money};
+
+ let price = Money::from_minor(**product.price as i64, iso::PLN).to_string();
+ let _description = product.short_description.as_str();
+ let name = product.name.as_str();
+ let img = product
+ .photos
+ .first()
+ .map(|photo| photo.url.as_str())
+ .unwrap_or_default();
+
+ div![
+ C!["w-full px-4 lg:px-0"],
div![
- C!["row"],
+ C!["p-3 bg-white rounded shadow-md"],
div![
- C!["one-half column"],
- p!["This index.html page is a placeholder with the CSS, font and favicon."]
+ div![
+ C!["relative w-full mb-3 h-62 lg:mb-0"],
+ img![attrs![
+ "src" => img,
+ "alt" => name,
+ "class" => "object-fill w-full h-full rounded"
+ ]],
+ ],
+ div![
+ C!["flex-auto p-2 justify-evenly"],
+ div![
+ C!["flex flex-wrap"],
+ div![
+ C!["flex items-center justify-between w-full min-w-0"],
+ h2![
+ C!["mr-auto text-lg cursor-pointer hover:text-gray-900"],
+ name
+ ]
+ ]
+ ],
+ div![
+ C!["flex items-center justify-between"],
+ a![C!["px-6 py-2 text-sm text-white bg-indigo-500 rounded-lg outline-none hover:bg-indigo-600 ring-indigo-300"], "Add to cart"],
+ div![C!["mt-1 text-xl font-semibold"], price],
+ ]
+ ]
]
]
]
diff --git a/web/static/output.css b/web/static/output.css
new file mode 100644
index 0000000..17036e5
--- /dev/null
+++ b/web/static/output.css
@@ -0,0 +1,822 @@
+/*
+! 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;
+}
+
+*, ::before, ::after {
+ --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;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --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;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+.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;
+ }
+}
+
+.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';
+ }
+}
+
+.mx-auto {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.mt-12 {
+ margin-top: 3rem;
+}
+
+.mb-3 {
+ margin-bottom: 0.75rem;
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem;
+}
+
+.inline-block {
+ display: inline-block;
+}
+
+.rounded-lg {
+ border-radius: 0.5rem;
+}
+
+.rounded-r {
+ border-top-right-radius: 0.25rem;
+ border-bottom-right-radius: 0.25rem;
+}
+
+.border {
+ border-width: 1px;
+}
+
+.border-transparent {
+ border-color: transparent;
+}
+
+.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));
+}
+
+.p-8 {
+ padding: 2rem;
+}
+
+.px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.leading-normal {
+ line-height: 1.5;
+}
+
+.tracking-tight {
+ 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));
+}
+
+.hover\:bg-blue-700:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(29 78 216 / var(--tw-bg-opacity));
+}
diff --git a/web/static/reset-password.html b/web/static/reset-password.html
new file mode 100644
index 0000000..782963e
--- /dev/null
+++ b/web/static/reset-password.html
@@ -0,0 +1,32 @@
+
+
+
+
+ Reset password
+
+
+
+
+
+
+
+
Trouble signing in?
+
+
Resetting your password is easy.
+
+
Just press the button below and follow the instructions. We’ll have you up and running in no time.
+
+
+ Reset Password
+
+
+
+
If you did not make this request then please ignore this email.
+
+
+
+
+
diff --git a/web/static/test-email.html b/web/static/test-email.html
new file mode 100644
index 0000000..63e78d9
--- /dev/null
+++ b/web/static/test-email.html
@@ -0,0 +1,25 @@
+
+
+
+
+ Welcome to {service_name}
+
+
+
+
+
+
+
+
Hi {login}
+
+
+ Welcome to {service_name} – we’re excited to have you on board and we’d love to say thank you on behalf
+ of our whole company for chosing us.
+
+
Take care,
+
{signature}
+
+
+
+
+
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
new file mode 100644
index 0000000..6bd0904
--- /dev/null
+++ b/web/tailwind.config.js
@@ -0,0 +1,24 @@
+module.exports = {
+ theme: {
+ extend: {
+ spacing: {
+ '96': '24rem',
+ },
+ },
+ },
+ content: [
+ "./static/*.{html,js}",
+ "./src/**/*.{html,js,rs}"
+ ],
+
+ plugins: [
+ require('@tailwindcss/forms'),
+ require('@tailwindcss/typography'),
+ require('@tailwindcss/line-clamp'),
+ require('@tailwindcss/aspect-ratio'),
+ require('tailwindcss-debug-screens'),
+ ],
+ experimental: {
+ optimizeUniversalDefaults: true
+ }
+}
diff --git a/web/yarn.lock b/web/yarn.lock
new file mode 100644
index 0000000..e62f899
--- /dev/null
+++ b/web/yarn.lock
@@ -0,0 +1,507 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@tailwindcss/aspect-ratio@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.0.tgz#c635dd7331cbcc1b111cebdc2647dd3493ebdd3e"
+ integrity sha512-WJu0I4PpqNPuutpaA9zDUq2JXR+lorZ7PbLcKNLmb6GL9/HLfC7w3CRsMhJF4BbYd/lkY6CfXOvkYpuGnZfkpQ==
+
+"@tailwindcss/forms@^0.5.1":
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.1.tgz#7fe86b9b67e6d91cb902e2d3f4ebe561cc057a13"
+ integrity sha512-QSwsFORnC2BAP0lRzQkz1pw+EzIiiPdk4e27vGQjyXkwJPeC7iLIRVndJzf9CJVbcrrIcirb/TfxF3gRTyFEVA==
+ dependencies:
+ mini-svg-data-uri "^1.2.3"
+
+"@tailwindcss/line-clamp@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/line-clamp/-/line-clamp-0.4.0.tgz#03353e31e77636b785f2336e8c978502cec1de81"
+ integrity sha512-HQZo6gfx1D0+DU3nWlNLD5iA6Ef4JAXh0LeD8lOGrJwEDBwwJNKQza6WoXhhY1uQrxOuU8ROxV7CqiQV4CoiLw==
+
+"@tailwindcss/typography@^0.5.2":
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.2.tgz#24b069dab24d7a2467d01aca0dd432cb4b29f0ee"
+ integrity sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==
+ dependencies:
+ lodash.castarray "^4.4.0"
+ lodash.isplainobject "^4.0.6"
+ lodash.merge "^4.6.2"
+
+acorn-node@^1.6.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+ integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
+ dependencies:
+ acorn "^7.0.0"
+ acorn-walk "^7.0.0"
+ xtend "^4.0.2"
+
+acorn-walk@^7.0.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+ integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
+
+acorn@^7.0.0:
+ version "7.4.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+ integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+anymatch@~3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+ integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+arg@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb"
+ integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==
+
+autoprefixer@^10.4.7:
+ version "10.4.7"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.7.tgz#1db8d195f41a52ca5069b7593be167618edbbedf"
+ integrity sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==
+ dependencies:
+ browserslist "^4.20.3"
+ caniuse-lite "^1.0.30001335"
+ fraction.js "^4.2.0"
+ normalize-range "^0.1.2"
+ picocolors "^1.0.0"
+ postcss-value-parser "^4.2.0"
+
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+braces@^3.0.2, braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
+browserslist@^4.20.3:
+ version "4.20.3"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf"
+ integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==
+ dependencies:
+ caniuse-lite "^1.0.30001332"
+ electron-to-chromium "^1.4.118"
+ escalade "^3.1.1"
+ node-releases "^2.0.3"
+ picocolors "^1.0.0"
+
+camelcase-css@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+ integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335:
+ version "1.0.30001338"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz#b5dd7a7941a51a16480bdf6ff82bded1628eec0d"
+ integrity sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ==
+
+chokidar@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+color-name@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+defined@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+ integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
+
+detective@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
+ integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==
+ dependencies:
+ acorn-node "^1.6.1"
+ defined "^1.0.0"
+ minimist "^1.1.1"
+
+didyoumean@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
+ integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
+dlv@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
+ integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
+electron-to-chromium@^1.4.118:
+ version "1.4.137"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f"
+ integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+fast-glob@^3.2.11:
+ version "3.2.11"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
+ integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fastq@^1.6.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+ integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
+ dependencies:
+ reusify "^1.0.4"
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+fraction.js@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
+ integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
+
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-core-module@^2.8.1:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
+ integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
+ dependencies:
+ has "^1.0.3"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+lilconfig@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"
+ integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==
+
+lodash.castarray@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
+ integrity sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+ integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
+lodash.merge@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+ integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+mini-svg-data-uri@^1.2.3:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
+ integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
+
+minimist@^1.1.1:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+ integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+nanoid@^3.3.3:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+ integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+
+node-releases@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476"
+ integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+ integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+object-hash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
+ integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+postcss-js@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
+ integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
+ dependencies:
+ camelcase-css "^2.0.1"
+
+postcss-load-config@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
+ integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
+ dependencies:
+ lilconfig "^2.0.5"
+ yaml "^1.10.2"
+
+postcss-nested@5.0.6:
+ version "5.0.6"
+ resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc"
+ integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==
+ dependencies:
+ postcss-selector-parser "^6.0.6"
+
+postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6:
+ version "6.0.10"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+ integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.4.12, postcss@^8.4.13:
+ version "8.4.13"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575"
+ integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==
+ dependencies:
+ nanoid "^3.3.3"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quick-lru@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+ integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+resolve@^1.22.0:
+ version "1.22.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
+ integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
+ dependencies:
+ is-core-module "^2.8.1"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+source-map-js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tailwindcss-debug-screens@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tailwindcss-debug-screens/-/tailwindcss-debug-screens-2.2.1.tgz#8dd0854a273daa4f30f4c383370872c6bca337cd"
+ integrity sha512-EMyA0CYBzqcZJHtVDvBfmYzfx3NxuK4qDyVO5wnzcGOrmJsv25D9xPpWefVTORTvhE6pCh90Z1WYnLUKsg3yMw==
+
+tailwindcss@^3.0.24:
+ version "3.0.24"
+ resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.24.tgz#22e31e801a44a78a1d9a81ecc52e13b69d85704d"
+ integrity sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==
+ dependencies:
+ arg "^5.0.1"
+ chokidar "^3.5.3"
+ color-name "^1.1.4"
+ detective "^5.2.0"
+ didyoumean "^1.2.2"
+ dlv "^1.1.3"
+ fast-glob "^3.2.11"
+ glob-parent "^6.0.2"
+ is-glob "^4.0.3"
+ lilconfig "^2.0.5"
+ normalize-path "^3.0.0"
+ object-hash "^3.0.0"
+ picocolors "^1.0.0"
+ postcss "^8.4.12"
+ postcss-js "^4.0.0"
+ postcss-load-config "^3.1.4"
+ postcss-nested "5.0.6"
+ postcss-selector-parser "^6.0.10"
+ postcss-value-parser "^4.2.0"
+ quick-lru "^5.1.1"
+ resolve "^1.22.0"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+util-deprecate@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+xtend@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+yaml@^1.10.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+ integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==