start show product
20
Cargo.lock
generated
@ -641,6 +641,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"gumdrop",
|
||||
"human-panic",
|
||||
"include_dir",
|
||||
"jemallocator",
|
||||
"log",
|
||||
"messagebus",
|
||||
@ -1900,6 +1901,25 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "482a2e29200b7eed25d7fdbd14423326760b7f6658d21a4cf12d55a50713c69f"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e074c19deab2501407c91ba1860fa3d6820bfde307db6d8cb851b55a10be89b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.1"
|
||||
|
@ -20,6 +20,8 @@ pub enum Error {
|
||||
Delete,
|
||||
#[error("Unable to find products for shopping cart")]
|
||||
ShoppingCartProducts,
|
||||
#[error("Product with id {0} can't be found")]
|
||||
Single(model::ProductId),
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
@ -52,6 +54,40 @@ FROM products
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<model::Product>")]
|
||||
pub struct FindProduct {
|
||||
pub product_id: model::ProductId,
|
||||
}
|
||||
|
||||
crate::db_async_handler!(FindProduct, find_product, Product, inner_find_product);
|
||||
|
||||
pub(crate) async fn find_product<'e, E>(msg: FindProduct, pool: E) -> Result<model::Product>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id,
|
||||
name,
|
||||
short_description,
|
||||
long_description,
|
||||
category,
|
||||
price,
|
||||
deliver_days_flag
|
||||
FROM products
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(msg.product_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("{e:?}");
|
||||
crate::Error::Product(Error::Single(msg.product_id))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[derive(Message, Debug)]
|
||||
#[rtype(result = "Result<model::Product>")]
|
||||
|
@ -68,5 +68,7 @@ async-trait = { version = "0.1", features = [] }
|
||||
|
||||
jemallocator = { version = "0.3", features = [] }
|
||||
|
||||
include_dir = { version = "0.7.2", features = [] }
|
||||
|
||||
# For rewrite into bus-based app
|
||||
messagebus = { version = "0.9.13" }
|
||||
|
1
api/assets/svg/cameras.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M501.801 204.776h-45.855a10.2 10.2 0 0 0-5.787 1.801l-55.461 38.216v-40.482c0-5.633-4.567-10.199-10.199-10.199H139.88c-4.836-28.448-29.646-50.182-59.45-50.182H20.398v-10.062c0-5.633-4.566-10.199-10.199-10.199S0 128.236 0 133.869v90.642c0 5.633 4.566 10.199 10.199 10.199s10.199-4.566 10.199-10.199v-10.063h39.319v163.683c0 5.633 4.566 10.199 10.199 10.199h314.582c5.632 0 10.199-4.566 10.199-10.199V337.65l55.461 38.215a10.197 10.197 0 0 0 5.787 1.801H501.8c5.632 0 10.199-4.566 10.199-10.199V214.975c.001-5.633-4.566-10.199-10.198-10.199zm-361.241 9.735h173.294v69.286H140.56v-69.286zM20.398 194.05v-29.721H80.43c18.488 0 34.082 12.634 38.601 29.721H20.398zm353.903 173.882H80.116V214.511h40.047v79.485c0 5.633 4.566 10.199 10.199 10.199h193.692c5.632 0 10.199-4.566 10.199-10.199v-79.485H374.3v153.421zm73.579-18.408-53.182-36.645v-43.314l53.182-36.646v116.605zm43.722 7.745h-23.324V225.174h23.324v132.095z"/><path d="M340.374 307.715c-14.776 0-26.797 12.022-26.797 26.797 0 14.776 12.022 26.797 26.797 26.797s26.797-12.021 26.797-26.797-12.021-26.797-26.797-26.797zm0 33.197c-3.529 0-6.399-2.87-6.399-6.399s2.87-6.399 6.399-6.399 6.399 2.87 6.399 6.398c0 3.528-2.87 6.4-6.399 6.4zM275.548 254.219h-4.249c-5.632 0-10.199 4.566-10.199 10.199s4.567 10.199 10.199 10.199h4.249c5.632 0 10.199-4.566 10.199-10.199s-4.567-10.199-10.199-10.199zM237.88 254.219h-75.713c-5.633 0-10.199 4.566-10.199 10.199s4.566 10.199 10.199 10.199h75.713c5.633 0 10.199-4.566 10.199-10.199s-4.566-10.199-10.199-10.199z"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
api/assets/svg/clothes.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" style="enable-background:new 0 0 511.999 511.999" xml:space="preserve"><path d="m506.784 168.058-95.761-95.761C400.333 61.607 386.12 55.72 371 55.72H140.997c-15.118 0-29.333 5.888-40.022 16.577l-95.76 95.761c-6.953 6.955-6.953 18.27 0 25.224l51.635 51.635c3.37 3.37 7.848 5.225 12.613 5.225s9.243-1.855 12.613-5.225l30.743-30.743V433.77c0 12.412 10.098 22.51 22.51 22.51h241.343c12.412 0 22.51-10.098 22.51-22.51V214.174l30.743 30.743a17.718 17.718 0 0 0 12.613 5.225 17.72 17.72 0 0 0 12.613-5.225l51.635-51.636c6.951-6.954 6.951-18.269-.002-25.223zm-215.292-96.17c-3.688 16.195-18.195 28.322-35.491 28.322-17.295 0-31.802-12.127-35.49-28.322h70.981zm-87.442 0c3.901 25.165 25.711 44.491 51.953 44.491 26.242 0 48.052-19.325 51.953-44.491h12.552c-3.995 32.051-31.389 56.942-64.504 56.942s-60.51-24.889-64.504-56.942h12.55zM383.012 433.77a6.348 6.348 0 0 1-6.341 6.341H135.328a6.348 6.348 0 0 1-6.341-6.341v-50.941h254.025v50.941zm0-67.11H128.987v-32.647h254.025v32.647zm0-48.816H128.987v-24.639h254.025v24.639zm0-40.809H128.987v-32.647h254.025v32.647zm112.339-95.186-51.635 51.635c-.403.403-.865.488-1.179.488s-.776-.085-1.179-.488l-34.643-34.643a13.846 13.846 0 0 0-15.132-3.01 13.844 13.844 0 0 0-8.571 12.827v19.56H128.987v-19.56c0-5.635-3.364-10.67-8.571-12.827a13.851 13.851 0 0 0-15.132 3.009l-34.643 34.643c-.403.403-.865.488-1.179.488s-.776-.085-1.179-.488l-51.635-51.635a1.669 1.669 0 0 1 0-2.359l95.761-95.76c7.636-7.636 17.789-11.842 28.588-11.842h34.215c4.071 40.989 38.747 73.111 80.79 73.111s76.72-32.121 80.79-73.111h34.209c10.8 0 20.953 4.205 28.589 11.842l95.761 95.76c.65.651.65 1.71 0 2.36z"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
api/assets/svg/drugstore.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406.833 406.833" style="enable-background:new 0 0 406.833 406.833" xml:space="preserve"><path d="M249.39 169.804h-39.451v-39.449c0-5.523-4.478-10-10-10h-47.221c-5.522 0-10 4.477-10 10v39.449h-39.451c-5.522 0-10 4.477-10 10v47.224c0 5.523 4.478 10 10 10h39.451v39.45c0 5.523 4.478 10 10 10h47.221c5.522 0 10-4.477 10-10v-39.45h39.451c5.522 0 10-4.477 10-10v-47.224c0-5.523-4.477-10-10-10zm-10 47.224h-39.451c-5.522 0-10 4.477-10 10v39.45h-27.221v-39.45c0-5.523-4.478-10-10-10h-39.451v-27.224h39.451c5.522 0 10-4.477 10-10v-39.449h27.221v39.449c0 5.523 4.478 10 10 10h39.451v27.224zm146.18-76.529a12.808 12.808 0 0 0 0-.392V10c0-5.523-4.478-10-10-10s-10 4.477-10 10v120.303h-34.174V85.64c0-20.563-16.729-37.292-37.291-37.292H58.554c-20.563 0-37.293 16.729-37.293 37.292v235.552c0 20.563 16.729 37.292 37.293 37.292h235.551c20.563 0 37.291-16.729 37.291-37.292v-44.664h34.174v120.305c0 5.523 4.478 10 10 10s10-4.477 10-10V266.725a12.808 12.808 0 0 0 0-.392V140.499zm-74.174 180.693c0 9.535-7.757 17.292-17.291 17.292H58.554c-9.535 0-17.293-7.757-17.293-17.292V85.64c0-9.535 7.758-17.292 17.293-17.292h235.551c9.534 0 17.291 7.757 17.291 17.292v235.552zm54.174-64.664h-34.174V150.303h34.174v106.225z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
api/assets/svg/fruits.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M503.807 279.708c-9.99-22.07-27.854-38.578-50.299-46.481-23.676-8.337-53.319-5.705-79.305 7.038-4.989 2.446-10.254 3.977-15.503 4.848-1.538-11.984.849-23.12 7.151-33.229a58.98 58.98 0 0 1 6.339-8.363c5.134-5.669 5.928-14.033 1.836-20.496-5.992-9.463-19.336-10.583-26.857-2.285a93.7 93.7 0 0 0-9.044 11.702c-7.549 11.575-14.847 28.691-13.597 50.58-3.259-.909-5.838-1.881-7.471-2.701-14.131-7.094-29.466-11.082-44.403-11.836-8.104-16.665-12.365-34.989-12.365-53.541 0-19.896-6.307-38.798-18.241-54.662-11.527-15.326-27.934-26.834-46.201-32.405a16.705 16.705 0 0 0-1.94-.468c-.262-.047-.522-.086-.783-.131 5.608-13.855 16.743-33.644 38.371-51.603 6.984-5.799 8.677-16.084 3.297-23.396-5.802-7.888-16.947-9.196-24.401-3.052-13.976 11.518-24.529 23.79-32.485 35.476-8.084-29.953-46.513-47.126-89.773-39.351-30.611 5.497-64.013 35.997-81.201 53.69A24.582 24.582 0 0 0 .384 80.469a24.585 24.585 0 0 0 13.596 17.81c19.287 9.186 53.321 23.147 81.982 23.146.548 0 1.088-.021 1.632-.032-12.318 15.519-19.35 34.562-19.35 53.551 0 28.603-10.112 56.481-28.473 78.501-23.233 27.861-36.028 63.213-36.028 99.544 0 85.756 69.768 155.525 155.524 155.525 31.515 0 62.193-9.598 87.965-27.277 29.754 29.55 56.703 30.331 75.766 24.205 4.56-1.466 8.758-3.851 12.575-7.139 3.82 3.288 8.016 5.673 12.577 7.139 5.732 1.843 12.174 3.061 19.195 3.061 23.05-.001 52.304-13.143 82.863-60.488 21.028-32.578 36.818-71.725 46.362-98.829 8.093-22.986 7.112-47.661-2.763-69.478zM40.711 73.226c20.64-19.499 40.106-32.188 53.402-34.577 26.907-4.832 49.301 4.337 51.353 15.762.893 4.972-2.138 11.114-8.316 16.849-8.172 7.587-20.69 13.344-34.342 15.797-13.298 2.389-35.965-2.733-62.097-13.831zm128.552 401.456c-67.099.001-121.69-54.591-121.69-121.693 0-28.427 10.008-56.085 28.18-77.877 23.422-28.088 36.321-63.661 36.321-100.167 0-15.072 7.619-30.44 20.378-41.109 10.273-8.59 23.019-13.435 36.981-14.272.541.051 1.081.086 1.615.086.917 0 1.818-.086 2.702-.23l.055-.001c4.281 0 8.664.369 13.108 1.114 23.695 7.661 39.543 29.404 39.543 54.413 0 19.991 3.861 39.775 11.283 58.246l-.103.036a88.009 88.009 0 0 0-4.154 1.582c-20.494 8.407-36.779 24.207-46.144 44.9-9.876 21.817-10.858 46.491-2.763 69.477 9.544 27.103 25.334 66.25 46.362 98.829a255.937 255.937 0 0 0 4.678 6.988 121.772 121.772 0 0 1-66.352 19.678zM474.658 337.95c-25.906 73.569-67.403 147.743-106.156 135.284-.815-.262-3.9-2.865-7.275-11.11a16.917 16.917 0 0 0-31.31.001c-3.374 8.244-6.461 10.846-7.274 11.109-16.321 5.244-33.13-4.878-49.055-23.846a17.061 17.061 0 0 0-1.22-1.719c-24.585-30.025-43.633-75.938-53.84-104.016l-.372-1.027-.618-1.715c-.353-.987-.705-1.975-1.052-2.961-5.185-14.724-4.59-30.452 1.672-44.289a55.669 55.669 0 0 1 7.097-11.666 52.825 52.825 0 0 1 7.973-8.07 51.867 51.867 0 0 1 15.643-8.785 46.981 46.981 0 0 1 3.22-.996c.325-.089.659-.161.987-.244a50.98 50.98 0 0 1 2.388-.55c.391-.079.787-.148 1.182-.218a57.14 57.14 0 0 1 3.571-.528 62.704 62.704 0 0 1 2.58-.237c12.573-.902 26.576 1.915 39.077 8.193 9.137 4.587 24.793 8.881 42.351 9.258.444.035.891.054 1.34.054.204 0 .41-.017.616-.025 13.979.064 29.039-2.402 42.914-9.205 17.738-8.697 38.113-10.805 53.172-5.503 13.656 4.81 24.565 14.939 30.713 28.522 6.268 13.835 6.861 29.564 1.676 44.289z"/></svg>
|
After Width: | Height: | Size: 3.3 KiB |
52
api/assets/svg/memory.svg
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 470 470" style="enable-background:new 0 0 470 470;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M415,155c4.143,0,7.5-3.358,7.5-7.5v-60c0-2.068-0.854-4.044-2.359-5.461l-85-80C333.749,0.729,331.91,0,330,0H55
|
||||
c-4.142,0-7.5,3.358-7.5,7.5v110c0,4.142,3.358,7.5,7.5,7.5h7.5v95H55c-4.142,0-7.5,3.358-7.5,7.5v235c0,4.142,3.358,7.5,7.5,7.5
|
||||
h125c4.142,0,7.5-3.358,7.5-7.5V455h95v7.5c0,4.142,3.357,7.5,7.5,7.5h125c4.143,0,7.5-3.358,7.5-7.5v-265
|
||||
c0-4.142-3.357-7.5-7.5-7.5h-2.5v-35H415z M407.5,140H405c-4.143,0-7.5,3.358-7.5,7.5v50c0,4.142,3.357,7.5,7.5,7.5h2.5v250h-110
|
||||
v-7.5c0-4.142-3.357-7.5-7.5-7.5H180c-4.142,0-7.5,3.358-7.5,7.5v7.5h-110V235H70c4.142,0,7.5-3.358,7.5-7.5v-110
|
||||
c0-4.142-3.358-7.5-7.5-7.5h-7.5V15h264.525L407.5,90.741V140z"/>
|
||||
<path d="M92.5,127.5v250c0,26.191,21.309,47.5,47.5,47.5h150c4.143,0,7.5-3.358,7.5-7.5c0-4.142-3.357-7.5-7.5-7.5H140
|
||||
c-17.92,0-32.5-14.58-32.5-32.5V180H340c4.143,0,7.5-3.358,7.5-7.5c0-4.142-3.357-7.5-7.5-7.5H107.5v-37.5
|
||||
c0-17.92,14.58-32.5,32.5-32.5h190c17.921,0,32.5,14.58,32.5,32.5v250c0,17.92-14.579,32.5-32.5,32.5h-10
|
||||
c-4.143,0-7.5,3.358-7.5,7.5c0,4.142,3.357,7.5,7.5,7.5h10c26.191,0,47.5-21.309,47.5-47.5v-250c0-26.191-21.309-47.5-47.5-47.5
|
||||
H140C113.809,80,92.5,101.309,92.5,127.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
api/assets/svg/pants.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.035 3.633a.6.6 0 0 1 .6-.633h12.73a.6.6 0 0 1 .6.633l-.933 16.8a.6.6 0 0 1-.6.567h-2.898a.6.6 0 0 1-.596-.53L12.596 9.065c-.083-.706-1.109-.706-1.192 0L10.062 20.47a.6.6 0 0 1-.596.53H6.568a.6.6 0 0 1-.6-.567l-.933-16.8Z" stroke="currentColor" stroke-width="1.5"/><path d="M5 7.5h1.5a2 2 0 0 0 2-2V3M18.5 7.5h-1a2 2 0 0 1-2-2V3" stroke="currentColor" stroke-width="1.5"/></svg>
|
After Width: | Height: | Size: 464 B |
1
api/assets/svg/phones.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="587.903" height="587.902" style="enable-background:new 0 0 587.903 587.902" xml:space="preserve"><path d="M136.29 506.487h233.927a3.99 3.99 0 0 0 3.979-3.987V87a3.99 3.99 0 0 0-3.979-3.988H136.29A3.986 3.986 0 0 0 132.302 87v415.51a3.977 3.977 0 0 0 3.988 3.977zm3.977-415.5h225.962v407.524H140.267V90.987zM253.506 509.939c-15.75 0-28.563 12.823-28.563 28.582 0 15.75 12.813 28.573 28.563 28.573 15.769 0 28.582-12.823 28.582-28.573.01-15.758-12.803-28.582-28.582-28.582zm0 49.391c-11.465 0-20.789-9.333-20.789-20.809 0-11.475 9.323-20.808 20.789-20.808 11.485 0 20.818 9.333 20.818 20.808 0 11.476-9.333 20.809-20.818 20.809z"/><path d="M260.611 527.534H246.42a3.893 3.893 0 0 0-3.882 3.892v14.201a3.891 3.891 0 0 0 3.882 3.882h14.191a3.893 3.893 0 0 0 3.892-3.882v-14.201c.01-2.152-1.74-3.892-3.892-3.892zm-3.882 14.2h-6.417v-6.426h6.417v6.426z"/><path d="M111.102 545.282c0 23.505 19.116 42.62 42.62 42.62H353.33c23.505 0 42.62-19.115 42.62-42.61l.593-502.663c0-23.504-19.125-42.62-42.63-42.62H154.295c-23.504 0-42.62 19.087-42.62 42.515l-.507 36.29c-2.744 2.4-4.131 6.617-4.131 12.556v13.971c0 5.881 1.368 10.079 4.055 12.499v4.092c-2.687 2.419-4.055 6.618-4.055 12.508v26.775c0 5.891 1.368 10.079 4.055 12.499v1.186c-2.687 2.419-4.055 6.608-4.055 12.489v26.775c0 5.891 1.368 10.079 4.055 12.498v318.623h.01zm11.351-331.12V187.196a7.693 7.693 0 0 0 4.054-6.78v-12.221a7.697 7.697 0 0 0-4.054-6.78v-27.177a7.709 7.709 0 0 0 4.054-6.79v-15.127a7.697 7.697 0 0 0-4.054-6.78V91.169a7.754 7.754 0 0 0 4.054-6.675l.583-41.874c0-15.004 12.202-27.206 27.215-27.206h199.607c15.004 0 27.215 12.202 27.215 27.196l-.583 502.662c0 15.004-12.202 27.206-27.215 27.206H153.722c-15.003 0-27.215-12.202-27.215-27.206v-324.13a7.697 7.697 0 0 0-4.054-6.78v-.2z"/><circle cx="253.239" cy="34.349" r="5.24"/><path d="M231.121 61.602h45.986a3.997 3.997 0 0 0 3.987-3.987c0-2.2-1.798-3.988-3.987-3.988h-45.986a3.985 3.985 0 0 0-3.987 3.988 3.978 3.978 0 0 0 3.987 3.987zM452.856 587.902h16.926c6.12 0 11.083-4.973 11.083-11.093V11.092c0-6.12-4.973-11.092-11.083-11.092h-16.926c-15.443 0-28.019 14.085-28.019 31.394v525.105c0 17.317 12.576 31.403 28.019 31.403zM440.262 31.394c0-8.807 5.651-15.979 12.594-15.979h12.594v557.073h-12.594c-6.952 0-12.594-7.172-12.594-15.979V31.394z"/><path d="M455.093 129.505a3.984 3.984 0 0 0-3.987 3.987v30.466a3.985 3.985 0 0 0 3.987 3.988 3.998 3.998 0 0 0 3.988-3.988v-30.466a3.991 3.991 0 0 0-3.988-3.987zM455.093 180.549a3.985 3.985 0 0 0-3.987 3.988v30.466c0 2.2 1.778 3.988 3.987 3.988a3.998 3.998 0 0 0 3.988-3.988v-30.466a3.992 3.992 0 0 0-3.988-3.988zM452.856 113.096c1.654 0 3.031-.909 3.873-2.209.144.019.21.144.354.144a2.665 2.665 0 0 0 2.658-2.659V87.793a2.659 2.659 0 0 0-2.658-2.658c-.144 0-.21.125-.354.144-.832-1.291-2.219-2.199-3.873-2.199a4.717 4.717 0 0 0-4.714 4.714v20.579a4.72 4.72 0 0 0 4.714 4.723zM450.8 87.793a2.06 2.06 0 0 1 2.056-2.056c.908 0 1.539.66 1.808 1.482-.048.21-.229.354-.229.573v20.579c0 .22.19.373.229.583-.269.822-.909 1.482-1.808 1.482a2.061 2.061 0 0 1-2.056-2.066V87.793z"/></svg>
|
After Width: | Height: | Size: 3.0 KiB |
1
api/assets/svg/speakers.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 403 403" style="enable-background:new 0 0 403 403" xml:space="preserve"><path d="M201.5 174.506c-54.287 0-98.453 44.166-98.453 98.453s44.166 98.453 98.453 98.453c54.287 0 98.453-44.166 98.453-98.453s-44.167-98.453-98.453-98.453zm0 181.905c-46.016 0-83.453-37.437-83.453-83.453s37.437-83.453 83.453-83.453 83.453 37.437 83.453 83.453-37.437 83.453-83.453 83.453z"/><path d="M201.5 202.41c-38.9 0-70.548 31.648-70.548 70.549s31.648 70.549 70.548 70.549c38.901 0 70.549-31.648 70.549-70.549S240.401 202.41 201.5 202.41zm0 126.098c-30.629 0-55.548-24.919-55.548-55.549S170.87 217.41 201.5 217.41c30.63 0 55.549 24.919 55.549 55.549s-24.919 55.549-55.549 55.549z"/><path d="M201.5 249.89c-12.72 0-23.068 10.348-23.068 23.068s10.349 23.068 23.068 23.068c12.72 0 23.069-10.348 23.069-23.068S214.22 249.89 201.5 249.89zm0 31.137c-4.449 0-8.068-3.619-8.068-8.068s3.62-8.068 8.068-8.068 8.069 3.619 8.069 8.068-3.621 8.068-8.069 8.068zM132.648 172.719c16.322 0 29.602-13.279 29.602-29.601s-13.279-29.602-29.602-29.602-29.601 13.279-29.601 29.602 13.279 29.601 29.601 29.601zm0-44.203c8.051 0 14.602 6.55 14.602 14.602s-6.55 14.601-14.602 14.601-14.601-6.55-14.601-14.601 6.55-14.602 14.601-14.602zM235.676 73.655c-12.72 0-23.068 10.349-23.068 23.069s10.349 23.068 23.068 23.068 23.068-10.348 23.068-23.068-10.348-23.069-23.068-23.069zm0 31.137c-4.449 0-8.068-3.619-8.068-8.068s3.619-8.069 8.068-8.069 8.068 3.62 8.068 8.069-3.619 8.068-8.068 8.068z"/><path d="M235.676 32.447c-35.442 0-64.277 28.834-64.277 64.277S200.234 161 235.676 161s64.276-28.834 64.276-64.277-28.834-64.276-64.276-64.276zm0 113.553c-27.171 0-49.277-22.105-49.277-49.277s22.106-49.277 49.277-49.277 49.276 22.105 49.276 49.277S262.847 146 235.676 146z"/><path d="M295.844 0H107.156C89.976 0 76 13.977 76 31.156v340.688C76 389.023 89.976 403 107.156 403h188.688c17.18 0 31.156-13.977 31.156-31.156V31.156C327 13.977 313.024 0 295.844 0zM312 371.844c0 8.909-7.248 16.156-16.156 16.156H107.156C98.247 388 91 380.752 91 371.844V31.156C91 22.248 98.247 15 107.156 15h188.688C304.752 15 312 22.248 312 31.156v340.688z"/></svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
api/assets/svg/sweets.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425.212 425.212"><path d="M191.67 240.859c25.369 0 50.738-9.656 70.052-28.969 38.625-38.626 38.625-101.476 0-140.103-38.627-38.626-101.477-38.626-140.104 0-18.711 18.711-29.016 43.589-29.016 70.051 0 23.1 7.854 44.991 22.296 62.629L2.929 316.436c-3.905 3.905-3.905 10.237 0 14.143 1.953 1.953 4.512 2.929 7.071 2.929s5.118-.976 7.071-2.929l111.974-111.974c18.158 14.835 40.391 22.254 62.625 22.254zm-55.909-154.93c15.416-15.416 35.659-23.121 55.909-23.121 20.245 0 40.497 7.709 55.909 23.121 30.828 30.829 30.828 80.99 0 111.818-30.827 30.828-80.989 30.829-111.818 0-14.935-14.934-23.158-34.79-23.158-55.909s8.223-40.975 23.158-55.909z"/><path d="M152.329 191.18a9.97 9.97 0 0 0 7.071-2.929l26.742-26.743c3.905-3.905 3.905-10.237 0-14.142-3.906-3.905-10.236-3.905-14.143 0l-26.742 26.743c-3.905 3.905-3.905 10.237 0 14.142a9.972 9.972 0 0 0 7.072 2.929zM420.212 278.43a10.002 10.002 0 0 0-10 0l-59.8 34.526c-4.556-20.601-22.954-36.061-44.905-36.061h-84.755c-21.951 0-40.349 15.46-44.905 36.061l-59.801-34.526a10.002 10.002 0 0 0-15 8.66v85.11a9.999 9.999 0 0 0 15 8.66l59.801-34.526c4.556 20.601 22.954 36.061 44.905 36.061h84.755c21.951 0 40.349-15.46 44.905-36.061l59.8 34.526a9.994 9.994 0 0 0 10 0 9.999 9.999 0 0 0 5-8.66v-85.11a9.999 9.999 0 0 0-5-8.66zm-299.166 76.449V304.41l43.708 25.235-43.708 25.234zm210.459-18.482c0 14.335-11.663 25.998-25.998 25.998h-84.755c-14.335 0-25.998-11.663-25.998-25.998v-13.504c0-14.335 11.663-25.998 25.998-25.998h84.755c14.335 0 25.998 11.663 25.998 25.998v13.504zm73.707 18.482-43.707-25.234 43.707-25.234v50.468zM360.208 252.363c12.9 0 25.029-5.023 34.151-14.146 9.122-9.123 14.146-21.251 14.146-34.151s-5.023-25.029-14.146-34.151c-9.122-9.122-21.251-14.146-34.151-14.146s-25.029 5.023-34.151 14.146c-9.122 9.123-14.146 21.251-14.146 34.151s5.023 25.029 14.146 34.151c9.122 9.122 21.251 14.146 34.151 14.146zm-20.009-68.306c5.345-5.345 12.45-8.288 20.009-8.288s14.664 2.943 20.009 8.288c5.345 5.344 8.288 12.451 8.288 20.009s-2.943 14.665-8.288 20.009c-5.345 5.345-12.45 8.288-20.009 8.288s-14.664-2.943-20.009-8.288c-5.345-5.344-8.288-12.451-8.288-20.009s2.944-14.665 8.288-20.009z"/></svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
api/assets/svg/vegetables.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 297" style="enable-background:new 0 0 297 297" xml:space="preserve"><path d="m282.869 48.219 13.093-26.362c2.44-4.912.435-10.872-4.476-13.311-4.914-2.441-10.871-.434-13.311 4.476l-13.093 26.362a23.827 23.827 0 0 0-27.437 4.591L200.432 81.39a9.927 9.927 0 0 0 2.623 15.896l6.652 3.304c-1.768 7.006-5.59 18.786-13.408 31.857a130.396 130.396 0 0 1-7.319 10.913c-1.717-14.388-8.69-26.798-19.872-36.497l1.764-.882a9.93 9.93 0 0 0-2.032-18.515l-64.209-16.052V43.302c0-5.484-4.446-9.93-9.93-9.93s-9.93 4.446-9.93 9.93v28.112L20.563 87.465a9.93 9.93 0 0 0-2.032 18.515l1.763.882C7.316 118.12 0 133.027 0 150.444c0 20.674 10.203 42.032 27.991 58.598 9.504 8.851 20.396 15.63 32.067 20.146-2.915 4.608-4.558 9.385-4.933 14.349-1.594 21.075 20.673 34.668 27.536 38.333 6.639 3.544 19.8 7.624 36.536 7.624 12.259 0 26.437-2.19 41.37-8.38 22.227-9.214 42.589-25.696 60.52-48.987 19.771-25.678 36.652-59.841 50.254-101.647l.195.199a9.932 9.932 0 0 0 16.934-5.565l7.322-52.26a23.837 23.837 0 0 0-12.923-24.635zM84.771 91.884v5.214c0 5.484 4.446 9.93 9.93 9.93s9.93-4.446 9.93-9.93v-5.214l33.349 8.338-11.854 5.927a9.929 9.929 0 0 0-5.49 8.882v19.799l-21.495-10.748a9.92 9.92 0 0 0-8.88 0l-21.494 10.747v-19.798a9.93 9.93 0 0 0-5.49-8.882l-11.855-5.927 33.349-8.338zm-64.911 58.56c0-14.898 8.059-25.982 20.409-33.594l8.638 4.319v29.728a9.928 9.928 0 0 0 14.37 8.881l31.424-15.712 31.425 15.712a9.924 9.924 0 0 0 9.661-.435 9.932 9.932 0 0 0 4.709-8.447v-29.728l8.638-4.319c12.35 7.612 20.409 18.697 20.409 33.594 0 5.319-1.013 10.789-2.909 16.193-13.303 10.834-29.074 19.525-47.259 25.945-18.564 6.555-33.018 13.491-43.489 20.89-32.673-8.393-56.026-37.46-56.026-63.027zm133.101 112.322c-29.512 12.236-54 5.291-60.946 1.584-8.873-4.738-17.598-12.547-17.086-19.315.286-3.784 4.814-17.397 51.057-33.724 48.994-17.298 74.845-47.393 87.904-69.593a145.044 145.044 0 0 0 9.431-19.109l1.483 4.411a9.93 9.93 0 0 0 15.524 4.662l13.037-10.181c-30.185 94.649-71.072 129.105-100.404 141.265zM276.123 70.098l-4.518 32.245-5.037-5.15a9.929 9.929 0 0 0-13.212-.884l-14.198 11.088-4.484-13.333a9.933 9.933 0 0 0-4.995-5.728l-5.452-2.707 27.498-27.646a4.005 4.005 0 0 1 4.625-.763l17.586 8.733a4.002 4.002 0 0 1 2.187 4.145z"/></svg>
|
After Width: | Height: | Size: 2.2 KiB |
@ -1,6 +1,6 @@
|
||||
pub mod api_v1;
|
||||
|
||||
use actix_web::web::ServiceConfig;
|
||||
use actix_web::web::{Path, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
pub use api_v1::{Error as V1Error, ShoppingCartError as V1ShoppingCartError};
|
||||
|
||||
@ -62,6 +62,35 @@ async fn pay_on_site() -> HttpResponse {
|
||||
HttpResponse::Ok().body("<h1>Pay on Site</h1>")
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(landing).configure(api_v1::configure);
|
||||
macro_rules! serve_svg {
|
||||
($name: expr) => {
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "image/svg+xml"))
|
||||
.body(include_bytes!(concat!("../../assets/svg/", $name, ".svg")).to_vec())
|
||||
};
|
||||
}
|
||||
|
||||
#[get("/svg/{file}.svg")]
|
||||
async fn svg(path: Path<String>) -> HttpResponse {
|
||||
let p = path.into_inner();
|
||||
match p.as_str() {
|
||||
"cameras" => serve_svg!("cameras"),
|
||||
"clothes" => serve_svg!("clothes"),
|
||||
"drugstore" => serve_svg!("drugstore"),
|
||||
"fruits" => serve_svg!("fruits"),
|
||||
"pants" => serve_svg!("pants"),
|
||||
"phones" => serve_svg!("phones"),
|
||||
"speakers" => serve_svg!("speakers"),
|
||||
"sweets" => serve_svg!("sweets"),
|
||||
"vegetables" => serve_svg!("vegetables"),
|
||||
"memory" => serve_svg!("memory"),
|
||||
_ => HttpResponse::NotFound().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config
|
||||
.service(landing)
|
||||
.service(svg)
|
||||
.configure(api_v1::configure);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use actix::Addr;
|
||||
use actix_web::web::{Data, Json, ServiceConfig};
|
||||
use actix_web::web::{Data, Json, Path, ServiceConfig};
|
||||
use actix_web::{get, post, HttpResponse};
|
||||
use config::SharedAppConfig;
|
||||
use database_manager::{query_db, Database};
|
||||
@ -33,6 +33,31 @@ async fn products(
|
||||
Ok(Json((products, photos, public_path).into()))
|
||||
}
|
||||
|
||||
#[get("/product/{id}")]
|
||||
async fn product(
|
||||
path: Path<model::RecordId>,
|
||||
db: Data<Addr<Database>>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> Result<Json<api::Product>> {
|
||||
let product_id: model::ProductId = path.into_inner().into();
|
||||
let db = db.into_inner();
|
||||
let public_path = {
|
||||
let l = config.lock();
|
||||
l.files().public_path()
|
||||
};
|
||||
|
||||
let product: model::Product =
|
||||
public_send_db!(owned, db, database_manager::FindProduct { product_id });
|
||||
let mut photos: Vec<model::ProductLinkedPhoto> = public_send_db!(
|
||||
owned,
|
||||
db,
|
||||
database_manager::PhotosForProducts {
|
||||
product_ids: vec![product.id]
|
||||
}
|
||||
);
|
||||
Ok(Json((product, &mut photos, public_path.as_str()).into()))
|
||||
}
|
||||
|
||||
#[get("/stocks")]
|
||||
async fn stocks(db: Data<Addr<Database>>) -> Result<HttpResponse> {
|
||||
public_send_db!(db.into_inner(), database_manager::AllStocks)
|
||||
@ -158,5 +183,9 @@ async fn handle_notification(
|
||||
}
|
||||
|
||||
pub(crate) fn configure(config: &mut ServiceConfig) {
|
||||
config.service(products).service(stocks).service(sign_in);
|
||||
config
|
||||
.service(product)
|
||||
.service(products)
|
||||
.service(stocks)
|
||||
.service(sign_in);
|
||||
}
|
||||
|
@ -161,6 +161,25 @@ pub struct Photo {
|
||||
pub unique_name: crate::UniqueName,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[derive(Clone, Debug, Hash, PartialOrd, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Category {
|
||||
pub name: String,
|
||||
pub key: String,
|
||||
pub svg: String,
|
||||
}
|
||||
|
||||
impl From<&crate::Category> for Category {
|
||||
fn from(crate::Category { name, key, svg }: &crate::Category) -> Self {
|
||||
Self {
|
||||
name: (*name).into(),
|
||||
key: (*key).into(),
|
||||
svg: (*svg).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[derive(Serialize, Deserialize, Debug, Hash)]
|
||||
pub struct Product {
|
||||
@ -168,7 +187,7 @@ pub struct Product {
|
||||
pub name: crate::ProductName,
|
||||
pub short_description: crate::ProductShortDesc,
|
||||
pub long_description: crate::ProductLongDesc,
|
||||
pub category: Option<crate::ProductCategory>,
|
||||
pub category: Option<Category>,
|
||||
pub price: crate::Price,
|
||||
pub deliver_days_flag: crate::Days,
|
||||
pub photos: Vec<Photo>,
|
||||
@ -195,7 +214,15 @@ impl<'path> From<(crate::Product, &mut Vec<ProductLinkedPhoto>, &'path str)> for
|
||||
name,
|
||||
short_description,
|
||||
long_description,
|
||||
category,
|
||||
category: category.and_then(|name| {
|
||||
crate::CATEGORIES.iter().find_map(|c| {
|
||||
if c.name == name.as_str() {
|
||||
Some(Category::from(c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}),
|
||||
price,
|
||||
deliver_days_flag,
|
||||
photos: photos
|
||||
|
@ -31,6 +31,57 @@ pub enum TransformError {
|
||||
|
||||
pub type RecordId = i32;
|
||||
|
||||
#[derive(Clone, Debug, Hash, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Category {
|
||||
pub name: &'static str,
|
||||
pub key: &'static str,
|
||||
pub svg: &'static str,
|
||||
}
|
||||
|
||||
pub const CATEGORIES: [Category; 8] = [
|
||||
Category {
|
||||
name: "Cameras",
|
||||
key: "cameras",
|
||||
svg: "/svg/cameras.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Drugstore",
|
||||
key: "drugstore",
|
||||
svg: "/svg/drugstore.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Speakers",
|
||||
key: "speakers",
|
||||
svg: "/svg/speakers.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Phones",
|
||||
key: "phones",
|
||||
svg: "/svg/phones.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Sweets",
|
||||
key: "sweets",
|
||||
svg: "/svg/sweets.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Memory",
|
||||
key: "memory",
|
||||
svg: "/svg/memory.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Pants",
|
||||
key: "pants",
|
||||
svg: "/svg/pants.svg",
|
||||
},
|
||||
Category {
|
||||
name: "Clothes",
|
||||
key: "clothes",
|
||||
svg: "/svg/clothes.svg",
|
||||
},
|
||||
];
|
||||
|
||||
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
|
||||
#[cfg_attr(feature = "db", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))]
|
||||
@ -398,18 +449,9 @@ where
|
||||
> {
|
||||
let value = <i32 as ::sqlx::decode::Decode<'r, sqlx::Postgres>>::decode(value)?;
|
||||
Ok(Days(
|
||||
(0..9)
|
||||
(0..7)
|
||||
.into_iter()
|
||||
.filter_map(|n| {
|
||||
eprintln!(
|
||||
"d {} {} {} {:?}",
|
||||
n,
|
||||
1 << n,
|
||||
value & 1 << n,
|
||||
Day::try_from(value & 1 << n).ok()
|
||||
);
|
||||
Day::try_from(value & 1 << n).ok()
|
||||
})
|
||||
.filter_map(|n| Day::try_from(value & 1 << n).ok())
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
@ -8,3 +8,7 @@ backend = "http://localhost:8080/api/v1"
|
||||
[[proxy]]
|
||||
rewrite = "/files"
|
||||
backend = "http://localhost:8080/files"
|
||||
|
||||
[[proxy]]
|
||||
rewrite = "/svg"
|
||||
backend = "http://localhost:8080/svg"
|
||||
|
@ -11,6 +11,16 @@ pub async fn fetch_products() -> fetch::Result<model::api::Products> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_product(product_id: model::ProductId) -> fetch::Result<model::api::Product> {
|
||||
Request::new(format!("/api/v1/product/{}", product_id))
|
||||
.method(Method::Get)
|
||||
.fetch()
|
||||
.await?
|
||||
.check_status()?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_me(access_token: AccessTokenString) -> fetch::Result<model::Account> {
|
||||
Request::new("/api/v1/me")
|
||||
.header(fetch::Header::bearer(access_token.as_str()))
|
||||
|
@ -10,6 +10,7 @@ use seed::prelude::*;
|
||||
use crate::model::Model;
|
||||
use crate::pages::{Msg, Page, PublicPage};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fetch_page {
|
||||
(public $model: expr, $page: ident, $ret: expr) => {{
|
||||
let p = match &mut $model.page {
|
||||
@ -21,6 +22,26 @@ macro_rules! fetch_page {
|
||||
_ => return $ret,
|
||||
}
|
||||
}};
|
||||
(public $model: expr, $page: ident) => {{
|
||||
let p = match &mut $model.page {
|
||||
crate::pages::Page::Public(p) => p,
|
||||
_ => return,
|
||||
};
|
||||
match p {
|
||||
crate::pages::PublicPage::$page(p) => p,
|
||||
_ => return,
|
||||
}
|
||||
}};
|
||||
(public page $page: expr, $page_name: ident) => {{
|
||||
let p = match $page {
|
||||
crate::pages::Page::Public(p) => p,
|
||||
_ => return,
|
||||
};
|
||||
match p {
|
||||
crate::pages::PublicPage::$page_name(p) => p,
|
||||
_ => return,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
@ -29,11 +50,9 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
.subscribe(Msg::UrlChanged);
|
||||
|
||||
Model {
|
||||
url: url.clone().set_path(&[] as &[&str]),
|
||||
token: LocalStorage::get("auth-token").ok(),
|
||||
page: Page::Public(PublicPage::Listing(pages::public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(proxy_public_listing),
|
||||
))),
|
||||
page: Page::init(url, orders),
|
||||
logo: seed::document()
|
||||
.query_selector("link[rel=icon]")
|
||||
.ok()
|
||||
@ -62,21 +81,22 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::UrlChanged(subs::UrlChanged(url)) => model.page = Page::init(url, orders),
|
||||
Msg::Public(pages::public::Msg::Listing(pages::public::listing::Msg::Shared(msg))) => {
|
||||
shared::update(msg, &mut model.shared, orders);
|
||||
}
|
||||
Msg::UrlChanged(subs::UrlChanged(url)) => model.page.page_changed(url),
|
||||
Msg::Public(pages::public::Msg::Listing(msg)) => {
|
||||
let page = fetch_page!(public model, Listing, ());
|
||||
let page = fetch_page!(public model, Listing);
|
||||
pages::public::listing::update(msg, page, &mut orders.proxy(proxy_public_listing));
|
||||
}
|
||||
Msg::Public(pages::public::Msg::Product(msg)) => {
|
||||
let page = fetch_page!(public model, Product);
|
||||
pages::public::product::update(msg, page, &mut orders.proxy(proxy_public_product))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: &Model) -> Node<Msg> {
|
||||
match &model.page {
|
||||
Page::Public(PublicPage::Listing(page)) => pages::public::listing::view(model, page)
|
||||
.map_msg(|msg| Msg::Public(pages::public::Msg::Listing(msg))),
|
||||
Page::Public(PublicPage::Listing(page)) => pages::public::listing::view(model, page),
|
||||
Page::Public(PublicPage::Product(page)) => pages::public::product::view(model, page),
|
||||
_ => empty![],
|
||||
}
|
||||
}
|
||||
@ -89,3 +109,7 @@ pub fn start() {
|
||||
fn proxy_public_listing(msg: pages::public::listing::Msg) -> Msg {
|
||||
Msg::Public(pages::public::Msg::Listing(msg))
|
||||
}
|
||||
|
||||
fn proxy_public_product(msg: pages::public::product::Msg) -> Msg {
|
||||
Msg::Public(pages::public::Msg::Product(msg))
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
use seed::Url;
|
||||
|
||||
use crate::Page;
|
||||
|
||||
pub struct Model {
|
||||
pub url: Url,
|
||||
pub token: Option<String>,
|
||||
pub page: Page,
|
||||
pub logo: Option<String>,
|
||||
|
@ -22,8 +22,8 @@ pub enum AdminPage {
|
||||
}
|
||||
|
||||
pub enum PublicPage {
|
||||
Listing(public::listing::Model),
|
||||
Product,
|
||||
Listing(public::listing::ListingPage),
|
||||
Product(public::product::ProductPage),
|
||||
ShoppingCart,
|
||||
Checkout,
|
||||
}
|
||||
@ -34,19 +34,20 @@ pub enum Page {
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>) -> Self {
|
||||
match url.remaining_path_parts().as_slice() {
|
||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Self {
|
||||
match url.clone().remaining_path_parts().as_slice() {
|
||||
[] => Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Listing(msg))),
|
||||
))),
|
||||
["products", rest @ ..] => {
|
||||
seed::log!(rest);
|
||||
Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Listing(msg))),
|
||||
)))
|
||||
}
|
||||
["products", _rest @ ..] => Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Listing(msg))),
|
||||
))),
|
||||
["product", _rest @ ..] => Self::Public(PublicPage::Product(public::product::init(
|
||||
url,
|
||||
&mut orders.proxy(|msg| Msg::Public(public::Msg::Product(msg))),
|
||||
))),
|
||||
["admin"] => Self::Admin(AdminPage::Landing),
|
||||
_ => Self::Public(PublicPage::Listing(public::listing::init(
|
||||
url,
|
||||
@ -54,36 +55,55 @@ impl Page {
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Filter {
|
||||
All,
|
||||
pub fn page_changed(&mut self, url: Url) {
|
||||
match url.clone().remaining_path_parts().as_slice() {
|
||||
[] => {
|
||||
let page = crate::fetch_page!(public page self, Listing);
|
||||
public::listing::page_changed(url, page);
|
||||
}
|
||||
["products", _rest @ ..] => {
|
||||
let page = crate::fetch_page!(public page self, Listing);
|
||||
public::listing::page_changed(url, page);
|
||||
}
|
||||
["page", ..] => {
|
||||
let page = crate::fetch_page!(public page self, Product);
|
||||
public::product::page_changed(url, page);
|
||||
}
|
||||
["admin"] => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct_urls!();
|
||||
impl<'a> Urls<'a> {
|
||||
fn home(self) -> Url {
|
||||
pub fn home(self) -> Url {
|
||||
self.base_url()
|
||||
}
|
||||
|
||||
// Public
|
||||
|
||||
fn listing(self) -> Url {
|
||||
pub fn listing(self) -> Url {
|
||||
self.base_url().add_path_part("products")
|
||||
}
|
||||
|
||||
fn product(self) -> Url {
|
||||
pub fn product(self) -> Url {
|
||||
self.base_url().add_path_part("product")
|
||||
}
|
||||
|
||||
fn shopping_cart(self) -> Url {
|
||||
pub fn shopping_cart(self) -> Url {
|
||||
self.base_url().add_path_part("shopping-cart")
|
||||
}
|
||||
|
||||
fn checkout(self) -> Url {
|
||||
pub fn checkout(self) -> Url {
|
||||
self.base_url().add_path_part("checkout")
|
||||
}
|
||||
|
||||
pub fn sign_in(self) -> Url {
|
||||
self.base_url().add_path_part("sign-in")
|
||||
}
|
||||
|
||||
// Admin
|
||||
pub fn admin_landing(self) -> Url {
|
||||
self.base_url()
|
||||
|
@ -1,15 +1,21 @@
|
||||
pub mod listing;
|
||||
pub mod product;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
Listing(listing::Msg),
|
||||
Product(product::Msg),
|
||||
}
|
||||
|
||||
pub mod layout {
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
pub fn view<Msg>(url: Url, content: Node<Msg>, categories: &[String]) -> Node<Msg> {
|
||||
pub fn view<Msg>(
|
||||
url: Url,
|
||||
content: Node<Msg>,
|
||||
categories: &[model::api::Category],
|
||||
) -> Node<Msg> {
|
||||
div![
|
||||
C!["flex"],
|
||||
div![
|
||||
@ -27,10 +33,10 @@ pub mod sidebar {
|
||||
|
||||
use crate::pages::Urls;
|
||||
|
||||
pub fn view<Msg>(url: Url, categories: &[String]) -> Node<Msg> {
|
||||
pub fn view<Msg>(url: Url, categories: &[model::api::Category]) -> Node<Msg> {
|
||||
let categories = categories
|
||||
.iter()
|
||||
.map(|category| item(url.clone(), category.as_str()));
|
||||
.map(|category| item(url.clone(), category));
|
||||
|
||||
div![
|
||||
C!["flex flex-col justify-between mt-6"],
|
||||
@ -38,14 +44,20 @@ pub mod sidebar {
|
||||
]
|
||||
}
|
||||
|
||||
fn item<Msg>(url: Url, category: &str) -> Node<Msg> {
|
||||
let url = Urls::new(url).listing().add_path_part(category);
|
||||
fn item<Msg>(url: Url, category: &model::api::Category) -> Node<Msg> {
|
||||
let url = Urls::new(url)
|
||||
.listing()
|
||||
.add_path_part(category.key.as_str());
|
||||
li![
|
||||
C!["flex items-center px-4 py-2 text-gray-700 rounded-md"],
|
||||
a![
|
||||
C!["flex items-center px-4 py-2 text-gray-700 rounded-md"],
|
||||
attrs!["href" => url],
|
||||
span![C!["mx-4 font-medium"], category]
|
||||
img![
|
||||
C!["w-6 h-6"],
|
||||
attrs!["src" => category.svg.as_str(), "style" => ""]
|
||||
],
|
||||
span![C!["mx-4 font-medium"], category.name.as_str()]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use model::api::Product;
|
||||
use seed::app::Orders;
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use crate::pages::Urls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
pub struct ListingPage {
|
||||
url: Url,
|
||||
pub products: HashMap<model::ProductId, model::api::Product>,
|
||||
pub errors: Vec<String>,
|
||||
pub categories: Vec<String>,
|
||||
pub categories: Vec<model::api::Category>,
|
||||
pub filters: HashSet<String>,
|
||||
pub visible_products: Vec<model::ProductId>,
|
||||
}
|
||||
@ -18,35 +21,55 @@ pub struct Model {
|
||||
pub enum Msg {
|
||||
FetchProducts,
|
||||
ProductFetched(fetch::Result<model::api::Products>),
|
||||
Shared(crate::shared::Msg),
|
||||
}
|
||||
|
||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
let filters = match url.clone().remaining_path_parts().as_slice() {
|
||||
["products", filters @ ..] => {
|
||||
filters
|
||||
.into_iter()
|
||||
.fold(HashSet::with_capacity(filters.len()), |mut s, filter| {
|
||||
s.insert(String::from(*filter));
|
||||
s
|
||||
})
|
||||
}
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> ListingPage {
|
||||
orders.send_msg(Msg::FetchProducts);
|
||||
let model = Model {
|
||||
url: url.to_base_url(),
|
||||
let model = ListingPage {
|
||||
filters: url_to_filters(url.clone()),
|
||||
url: url.set_path(&[] as &[&str]),
|
||||
products: Default::default(),
|
||||
errors: vec![],
|
||||
categories: vec![],
|
||||
filters,
|
||||
visible_products: vec![],
|
||||
};
|
||||
seed::log!(&model);
|
||||
model
|
||||
}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
fn url_to_filters(mut url: Url) -> HashSet<String> {
|
||||
match url.remaining_path_parts().as_slice() {
|
||||
["products", filters @ ..] => {
|
||||
filters
|
||||
.iter()
|
||||
.fold(HashSet::with_capacity(filters.len()), |mut s, filter| {
|
||||
s.insert(String::from(*filter));
|
||||
s
|
||||
})
|
||||
}
|
||||
_ => HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_changed(url: Url, model: &mut ListingPage) {
|
||||
model.filters = url_to_filters(url);
|
||||
filter_products(model)
|
||||
}
|
||||
|
||||
fn filter_products(model: &mut ListingPage) {
|
||||
model.visible_products = model
|
||||
.products
|
||||
.iter()
|
||||
.filter_map(|(_, p)| {
|
||||
p.category
|
||||
.as_ref()
|
||||
.filter(|c| model.filters.contains(c.key.as_str()))
|
||||
.map(|_| p.id)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut ListingPage, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::FetchProducts => {
|
||||
orders.skip().perform_cmd({
|
||||
@ -58,14 +81,14 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
.0
|
||||
.iter()
|
||||
.fold(HashSet::new(), |mut set, p| {
|
||||
if let Some(category) = p.category.as_deref() {
|
||||
set.insert(String::from(category));
|
||||
if let Some(category) = p.category.as_ref().cloned() {
|
||||
set.insert(category);
|
||||
}
|
||||
set
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
model.categories.sort();
|
||||
model.categories.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
model.products = {
|
||||
let len = products.0.len();
|
||||
products
|
||||
@ -76,45 +99,38 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
m
|
||||
})
|
||||
};
|
||||
model.visible_products = model
|
||||
.products
|
||||
.iter()
|
||||
.filter_map(|(_, p)| {
|
||||
p.category
|
||||
.as_deref()
|
||||
.filter(|c| model.filters.contains(*c))
|
||||
.map(|_| p.id)
|
||||
})
|
||||
.collect();
|
||||
filter_products(model);
|
||||
}
|
||||
Msg::ProductFetched(Err(_e)) => {
|
||||
model.errors.push("Failed to load products".into());
|
||||
}
|
||||
Msg::Shared(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &crate::Model, page: &Model) -> Node<Msg> {
|
||||
let products = page
|
||||
.visible_products
|
||||
.iter()
|
||||
.filter_map(|id| page.products.get(id))
|
||||
.map(product);
|
||||
pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
|
||||
let products: Vec<Node<Msg>> = if page.visible_products.is_empty() {
|
||||
page.products.values().map(|p| product(model, p)).collect()
|
||||
} else {
|
||||
page.visible_products
|
||||
.iter()
|
||||
.filter_map(|id| page.products.get(id))
|
||||
.map(|p| product(model, p))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let content = div![
|
||||
C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
|
||||
products
|
||||
]
|
||||
.map_msg(|msg: Msg| crate::Msg::Public(super::Msg::Listing(msg)));
|
||||
|
||||
div![
|
||||
crate::shared::view::public_navbar(model),
|
||||
super::layout::view(
|
||||
page.url.clone(),
|
||||
div![
|
||||
C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"],
|
||||
products
|
||||
],
|
||||
&page.categories
|
||||
)
|
||||
super::layout::view(page.url.clone(), content, &page.categories)
|
||||
]
|
||||
}
|
||||
|
||||
fn product(product: &model::api::Product) -> Node<Msg> {
|
||||
fn product(model: &crate::Model, product: &model::api::Product) -> Node<Msg> {
|
||||
use rusty_money::{iso, Money};
|
||||
|
||||
let price = Money::from_minor(**product.price as i64, iso::PLN).to_string();
|
||||
@ -126,10 +142,15 @@ fn product(product: &model::api::Product) -> Node<Msg> {
|
||||
.map(|photo| photo.url.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
let url = Urls::new(model.url.clone())
|
||||
.product()
|
||||
.add_path_part((*product.id as i32).to_string());
|
||||
|
||||
div![
|
||||
C!["w-full px-4 lg:px-0"],
|
||||
div![
|
||||
C!["p-3 bg-white rounded shadow-md"],
|
||||
a![attrs!["href" => url],
|
||||
div![
|
||||
div![
|
||||
C!["relative w-full mb-3 h-62 lg:mb-0"],
|
||||
@ -153,11 +174,11 @@ fn product(product: &model::api::Product) -> Node<Msg> {
|
||||
],
|
||||
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"],
|
||||
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],
|
||||
]
|
||||
]
|
||||
]
|
||||
]]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
84
web/src/pages/public/product.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
ProductFetched(fetch::Result<model::api::Product>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProductPage {
|
||||
pub product_id: Option<model::ProductId>,
|
||||
pub product: Option<model::api::Product>,
|
||||
}
|
||||
|
||||
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>) -> ProductPage {
|
||||
let product_id = match url.remaining_path_parts().as_slice() {
|
||||
["product", id] => id.parse::<model::RecordId>().unwrap_or_default().into(),
|
||||
_ => return ProductPage::default(),
|
||||
};
|
||||
orders.perform_cmd(async move {
|
||||
Msg::ProductFetched(crate::api::public::fetch_product(product_id).await)
|
||||
});
|
||||
ProductPage {
|
||||
product_id: Some(product_id),
|
||||
product: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_changed(url: Url, model: &mut ProductPage) {}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut ProductPage, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ProductFetched(Ok(product)) => {
|
||||
model.product = Some(product);
|
||||
}
|
||||
Msg::ProductFetched(Err(e)) => {
|
||||
seed::error!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(_model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
|
||||
let product = match page.product.as_ref() {
|
||||
None => return empty!(),
|
||||
Some(product) => product,
|
||||
};
|
||||
let images = product
|
||||
.photos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, img)| image(idx, img).map_msg(map_to_global));
|
||||
div![
|
||||
C!["max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6"],
|
||||
div![
|
||||
C!["flex flex-col md:flex-row -mx-4"],
|
||||
div![
|
||||
C!["md:flex-1 px-4"], attrs!["id" => "photos"],
|
||||
div![
|
||||
attrs!["x-data" => "{ image: 1}"],
|
||||
div![C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4"], images]
|
||||
]
|
||||
],
|
||||
div![
|
||||
C!["md:flex-1 px-4"], attrs!["id" => "details"],
|
||||
h2![
|
||||
C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"],
|
||||
product.name.as_str()
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
fn image(idx: usize, img: &model::api::Photo) -> Node<Msg> {
|
||||
div![
|
||||
C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4 flex items-center justify-center"],
|
||||
attrs!["x-show" => format!("image == {}", idx + 1)],
|
||||
img![attrs!["src" => img.url.as_str()]]
|
||||
]
|
||||
}
|
||||
|
||||
fn map_to_global(msg: Msg) -> crate::Msg {
|
||||
crate::pages::Msg::Public(crate::pages::public::Msg::Product(msg))
|
||||
}
|
@ -1,23 +1,26 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
pub fn public_navbar<Msg>(model: &crate::Model) -> Node<Msg> {
|
||||
use crate::pages::Urls;
|
||||
use crate::Msg;
|
||||
|
||||
pub fn public_navbar(model: &crate::Model) -> Node<Msg> {
|
||||
header![
|
||||
C!["container flex justify-around py-8 mx-auto bg-white"],
|
||||
div![C!["flex items-center"], logo(model),],
|
||||
div![C!["flex items-center"], logo(model)],
|
||||
div![
|
||||
C!["items-center hidden space-x-8 lg:flex"],
|
||||
navbar_item(div![C![""], "Home"], "/"),
|
||||
navbar_item(div![C![""], "Home"], Urls::new(model.url.clone()).home()),
|
||||
],
|
||||
div![
|
||||
C!["flex items-center space-x-2"],
|
||||
navbar_item(account(), "/sign-in"),
|
||||
navbar_item(bag(), "/cart")
|
||||
navbar_item(account(), Urls::new(model.url.clone()).sign_in()),
|
||||
navbar_item(bag(), Urls::new(model.url.clone()).shopping_cart())
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
fn navbar_item<Msg>(name: Node<Msg>, path: &str) -> Node<Msg> {
|
||||
fn navbar_item(name: Node<Msg>, path: Url) -> Node<Msg> {
|
||||
a![
|
||||
attrs!["href" => path],
|
||||
C!["px-4 py-2 font-semibold text-gray-600 rounded"],
|
||||
@ -25,7 +28,7 @@ fn navbar_item<Msg>(name: Node<Msg>, path: &str) -> Node<Msg> {
|
||||
]
|
||||
}
|
||||
|
||||
fn logo<Msg>(model: &crate::Model) -> Node<Msg> {
|
||||
fn logo(model: &crate::Model) -> Node<Msg> {
|
||||
a![
|
||||
attrs!["href" => "/"],
|
||||
match model.logo.as_deref() {
|
||||
@ -38,7 +41,7 @@ fn logo<Msg>(model: &crate::Model) -> Node<Msg> {
|
||||
]
|
||||
}
|
||||
|
||||
fn bag<Msg>() -> Node<Msg> {
|
||||
fn bag() -> Node<Msg> {
|
||||
svg![
|
||||
attrs![
|
||||
"width" => "32px",
|
||||
@ -56,7 +59,7 @@ fn bag<Msg>() -> Node<Msg> {
|
||||
]
|
||||
}
|
||||
|
||||
fn account<Msg>() -> Node<Msg> {
|
||||
fn account() -> Node<Msg> {
|
||||
svg![
|
||||
attrs![
|
||||
"xmlns" => "http://www.w3.org/2000/svg",
|
||||
|