From bf6cb6f5870089dfe2f16ad4370e353208400c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Mon, 9 May 2022 16:17:27 +0200 Subject: [PATCH] Display listing --- .env | 7 +- Cargo.lock | 214 +++-- actors/database_manager/src/photos.rs | 12 +- actors/email_manager/Cargo.toml | 5 + .../email_manager/assets/reset-password.html | 34 + actors/email_manager/assets/style.css | 9 + actors/email_manager/assets/welcome.html | 27 + actors/email_manager/src/lib.rs | 134 ++- actors/fs_manager/src/lib.rs | 8 +- api/Cargo.toml | 2 +- api/src/main.rs | 13 +- api/src/routes/admin/api_v1/products.rs | 9 +- api/src/routes/admin/api_v1/uploads.rs | 8 +- api/src/routes/public/api_v1/unrestricted.rs | 11 +- ...0220509124526_add_unique_name_to_files.sql | 2 + migrations/20220509133117_update_photos.sql | 1 + shared/config/src/lib.rs | 90 +- shared/model/src/api.rs | 51 +- shared/model/src/lib.rs | 47 +- web/Cargo.toml | 4 + web/Trunk.toml | 8 + web/index.html | 4 +- web/package.json | 12 + web/src/api.rs | 1 + web/src/api/public.rs | 11 + web/src/input.css | 3 + web/src/lib.rs | 27 +- web/src/pages/public/listing.rs | 92 +- web/static/output.css | 822 ++++++++++++++++++ web/static/reset-password.html | 32 + web/static/test-email.html | 25 + web/tailwind.config.js | 24 + web/yarn.lock | 507 +++++++++++ 33 files changed, 2044 insertions(+), 212 deletions(-) create mode 100644 actors/email_manager/assets/reset-password.html create mode 100644 actors/email_manager/assets/style.css create mode 100644 actors/email_manager/assets/welcome.html create mode 100644 migrations/20220509124526_add_unique_name_to_files.sql create mode 100644 migrations/20220509133117_update_photos.sql create mode 100644 web/package.json create mode 100644 web/src/api.rs create mode 100644 web/src/api/public.rs create mode 100644 web/src/input.css create mode 100644 web/static/output.css create mode 100644 web/static/reset-password.html create mode 100644 web/static/test-email.html create mode 100644 web/tailwind.config.js create mode 100644 web/yarn.lock 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==