More images, translations, product page

This commit is contained in:
Adrian Woźniak 2022-05-12 16:05:58 +02:00
parent 19115f0dd9
commit ef903c6f31
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
47 changed files with 435 additions and 87 deletions

4
Cargo.lock generated
View File

@ -629,6 +629,7 @@ dependencies = [
"actix-web-httpauth",
"actix-web-opentelemetry",
"async-trait",
"bytes",
"cart_manager",
"chrono",
"config",
@ -1133,6 +1134,7 @@ dependencies = [
"actix 0.13.0",
"actix-rt",
"actix-web",
"bytes",
"config",
"database_manager",
"dotenv",
@ -1396,6 +1398,8 @@ version = "0.1.0"
dependencies = [
"actix 0.13.0",
"actix-rt",
"actix-web",
"bytes",
"chrono",
"config",
"log",

View File

@ -7,7 +7,10 @@ edition = "2021"
model = { path = "../../shared/model" }
config = { path = "../../shared/config" }
bytes = { version = "1.1.0" }
actix = { version = "0.13", features = [] }
actix-web = { version = "4.0.1" }
actix-rt = { version = "2.7", features = [] }
thiserror = { version = "1.0.31" }

View File

@ -157,7 +157,7 @@ pub struct WriteResult {
#[rtype(result = "Result<WriteResult>")]
pub struct WriteFile {
pub file_name: String,
pub stream: tokio::sync::mpsc::UnboundedReceiver<u8>,
pub stream: tokio::sync::mpsc::UnboundedReceiver<actix_web::web::Bytes>,
}
fs_async_handler!(WriteFile, write_file, WriteResult);
@ -210,7 +210,7 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
if counter % 100_000 == 0 {
log::debug!("Wrote {} for {:?}", counter, file_name);
}
match file.write(&[b]) {
match file.write(&b) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::StorageFull => return Err(Error::NoSpace),
Err(e) => return Err(Error::CantWrite(e)),
@ -221,6 +221,6 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
Ok(WriteResult {
file_name: FileName::new(file_name),
unique_name: UniqueName::new(unique_name),
local_path: LocalPath::from(path.to_str().unwrap_or_default().to_string()),
local_path: LocalPath::new(path.to_str().unwrap_or_default()),
})
}

View File

@ -17,6 +17,8 @@ fs_manager = { path = "../actors/fs_manager" }
human-panic = { version = "1.0.3" }
bytes = { version = "1.1.0" }
actix = { version = "0.13", features = [] }
actix-rt = { version = "2.7", features = [] }
actix-web = { version = "4.0", features = [] }

1
api/assets/svg/plate.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288.643 288.643" style="enable-background:new 0 0 288.643 288.643" xml:space="preserve"><circle style="fill:#fff" cx="144.321" cy="144.322" r="136.821"/><path style="fill:#414042" d="M144.321.001C64.742.001 0 64.743 0 144.322s64.742 144.321 144.321 144.321S288.643 223.9 288.643 144.322 223.9.001 144.321.001zm0 273.642C73.014 273.643 15 215.63 15 144.322S73.014 15.001 144.321 15.001s129.321 58.013 129.321 129.321-58.013 129.321-129.321 129.321z"/><path style="fill:#414042" d="M144.321 47.206c-53.551 0-97.117 43.566-97.117 97.117s43.566 97.117 97.117 97.117a96.968 96.968 0 0 0 64.523-24.533 7.498 7.498 0 0 0 .619-10.588 7.5 7.5 0 0 0-10.588-.619 81.994 81.994 0 0 1-54.555 20.74c-45.279 0-82.117-36.837-82.117-82.117 0-45.279 36.838-82.117 82.117-82.117s82.117 36.837 82.117 82.117c0 11.936-2.502 23.442-7.437 34.197a7.5 7.5 0 0 0 3.69 9.944 7.498 7.498 0 0 0 9.944-3.69c5.84-12.732 8.802-26.342 8.802-40.452.003-53.55-43.564-97.116-97.115-97.116z"/></svg>

After

Width:  |  Height:  |  Size: 1015 B

View File

@ -51,13 +51,11 @@ async fn upload_product_image(
};
let read = async {
while let Some(Ok(data)) = field.next().await {
for b in data {
if let Err(e) = tx.send(b) {
if let Err(e) = tx.send(data) {
log::error!("{e:?}");
return Err(UploadError::InvalidName);
}
}
}
Ok(())
};

View File

@ -84,6 +84,7 @@ async fn svg(path: Path<String>) -> HttpResponse {
"sweets" => serve_svg!("sweets"),
"vegetables" => serve_svg!("vegetables"),
"memory" => serve_svg!("memory"),
"plates" => serve_svg!("plate"),
_ => HttpResponse::NotFound().finish(),
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -9,6 +9,8 @@ config = { path = "../shared/config" }
database_manager = { path = "../actors/database_manager", features = ["dummy"] }
fs_manager = { path = "../actors/fs_manager", features = [] }
bytes = { version = "1.1.0" }
actix = { version = "0.13", features = [] }
actix-rt = { version = "2.7", features = [] }
actix-web = { version = "4.0", features = [] }

View File

@ -1,4 +1,5 @@
use actix::{Actor, Addr};
use actix_web::web::BytesMut;
use config::SharedAppConfig;
use database_manager::{query_db, Database};
use fs_manager::query_fs;
@ -18,15 +19,21 @@ async fn create_photo(
tokio::fs::File::open(std::path::Path::new("./assets/examples/images").join(file))
.await
.unwrap();
while let Ok(b) = file.read_u8().await {
tx.send(b).unwrap();
let mut buffer = BytesMut::with_capacity(1024);
while let Ok(len) = file.read_buf(&mut buffer).await {
if len == 0 {
break;
}
tx.send(actix_web::web::Bytes::from(buffer)).unwrap();
buffer = BytesMut::with_capacity(1024);
}
};
let write = async {
let fs_manager::WriteResult {
unique_name: _,
unique_name,
local_path,
file_name,
} = query_fs!(
fs,
fs_manager::WriteFile {
@ -39,7 +46,8 @@ async fn create_photo(
db,
database_manager::CreatePhoto {
local_path,
file_name: model::FileName::new(file)
file_name,
unique_name
},
crate::Error::WritePhoto(file.into())
);
@ -135,7 +143,25 @@ pub(crate) async fn create_photos(
seed.clone(),
fs.clone(),
"pexels-Venus-HD-Make-up-and-perfume-2587370.webp"
)
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-agnese-lunecka-10322857.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-agnese-lunecka-11179383.webp"
),
create_photo(
db.clone(),
seed.clone(),
fs.clone(),
"pexels-agnese-lunecka-11328773.webp"
),
);
results.0.unwrap();
results.1.unwrap();
@ -148,5 +174,6 @@ pub(crate) async fn create_photos(
results.8.unwrap();
results.9.unwrap();
results.10.unwrap();
results.11.unwrap();
Ok(())
}

View File

@ -122,7 +122,25 @@ pub(crate) async fn create_product_photos(
seed.clone(),
"Venus HD Professional",
"pexels-Venus-HD-Make-up-and-perfume-2587370.webp"
)
),
create_product_photo(
db.clone(),
seed.clone(),
"Fancy Plate",
"pexels-agnese-lunecka-10322857.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Fancy Plate",
"pexels-agnese-lunecka-11179383.webp"
),
create_product_photo(
db.clone(),
seed.clone(),
"Fancy Plate",
"pexels-agnese-lunecka-11328773.webp"
),
);
results.0.unwrap();
results.1.unwrap();

View File

@ -54,21 +54,77 @@ pub(crate) async fn create_products(
}
let results = tokio::join!(
create_product(db.clone(), seed.clone(), "Nikon", "Cameras",),
create_product(db.clone(), seed.clone(), "Bonoid CBD", "Drugstore",),
create_product(db.clone(), seed.clone(), "Casio Speaker", "Speakers",),
create_product(db.clone(), seed.clone(), "Eprism Studio", "Drugstore",),
create_product(db.clone(), seed.clone(), "Best Phones 2022", "Phones",),
create_product(db.clone(), seed.clone(), "Sweet cake", "Sweets",),
create_product(db.clone(), seed.clone(), "Lexal 128G", "Memory",),
create_product(db.clone(), seed.clone(), "Fujifilm X-T10", "Cameras",),
create_product(db.clone(), seed.clone(), "Sweet Tower", "Sweets",),
create_product(db.clone(), seed.clone(), "Nikon Lenses", "Cameras",),
create_product(
db.clone(),
seed.clone(),
"Nikon",
model::Category::CAMERAS_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Bonoid CBD",
model::Category::DRUGSTORE_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Casio Speaker",
model::Category::SPEAKERS_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Eprism Studio",
model::Category::DRUGSTORE_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Best Phones 2022",
model::Category::PHONES_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Sweet cake",
model::Category::SWEETS_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Lexal 128G",
model::Category::MEMORY_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Fujifilm X-T10",
model::Category::CAMERAS_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Sweet Tower",
model::Category::SWEETS_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Nikon Lenses",
model::Category::CAMERAS_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Venus HD Professional",
"Drugstore",
model::Category::DRUGSTORE_NAME,
),
create_product(
db.clone(),
seed.clone(),
"Fancy Plate",
model::Category::PLATES_NAME,
)
);
results.0.unwrap();

View File

@ -39,46 +39,86 @@ pub struct Category {
pub svg: &'static str,
}
pub const CATEGORIES: [Category; 8] = [
impl Category {
pub const CAMERAS_NAME: &'static str = "Cameras";
pub const CAMERAS_KEY: &'static str = "cameras";
pub const DRUGSTORE_NAME: &'static str = "Drugstore";
pub const DRUGSTORE_KEY: &'static str = "drugstore";
pub const SPEAKERS_NAME: &'static str = "Speakers";
pub const SPEAKERS_KEY: &'static str = "speakers";
pub const PHONES_NAME: &'static str = "Phones";
pub const PHONES_KEY: &'static str = "phones";
pub const SWEETS_NAME: &'static str = "Sweets";
pub const SWEETS_KEY: &'static str = "sweets";
pub const MEMORY_NAME: &'static str = "Memory";
pub const MEMORY_KEY: &'static str = "memory";
pub const PANTS_NAME: &'static str = "Pants";
pub const PANTS_KEY: &'static str = "pants";
pub const CLOTHES_NAME: &'static str = "Clothes";
pub const CLOTHES_KEY: &'static str = "clothes";
pub const PLATES_NAME: &'static str = "Plates";
pub const PLATES_KEY: &'static str = "plates";
}
macro_rules! category_svg {
($name: expr) => {
concat!("/svg/", $name, ".svg")
};
}
pub const CATEGORIES: [Category; 9] = [
Category {
name: "Cameras",
key: "cameras",
svg: "/svg/cameras.svg",
name: Category::CAMERAS_NAME,
key: Category::CAMERAS_KEY,
svg: category_svg!("cameras"),
},
Category {
name: "Drugstore",
key: "drugstore",
svg: "/svg/drugstore.svg",
name: Category::DRUGSTORE_NAME,
key: Category::DRUGSTORE_KEY,
svg: category_svg!("drugstore"),
},
Category {
name: "Speakers",
key: "speakers",
svg: "/svg/speakers.svg",
name: Category::SPEAKERS_NAME,
key: Category::SPEAKERS_KEY,
svg: category_svg!("speakers"),
},
Category {
name: "Phones",
key: "phones",
svg: "/svg/phones.svg",
name: Category::PHONES_NAME,
key: Category::PHONES_KEY,
svg: category_svg!("phones"),
},
Category {
name: "Sweets",
key: "sweets",
svg: "/svg/sweets.svg",
name: Category::SWEETS_NAME,
key: Category::SWEETS_KEY,
svg: category_svg!("sweets"),
},
Category {
name: "Memory",
key: "memory",
svg: "/svg/memory.svg",
name: Category::MEMORY_NAME,
key: Category::MEMORY_KEY,
svg: category_svg!("memory"),
},
Category {
name: "Pants",
key: "pants",
svg: "/svg/pants.svg",
name: Category::PANTS_NAME,
key: Category::PANTS_KEY,
svg: category_svg!("pants"),
},
Category {
name: "Clothes",
key: "clothes",
svg: "/svg/clothes.svg",
name: Category::CLOTHES_NAME,
key: Category::CLOTHES_KEY,
svg: category_svg!("clothes"),
},
Category {
name: Category::PLATES_NAME,
key: Category::PLATES_KEY,
svg: category_svg!("plates"),
},
];
@ -386,6 +426,31 @@ pub enum Day {
Sunday = 1 << 6,
}
impl Day {
pub fn name(&self) -> &str {
match self {
Self::Monday => "Monday",
Self::Tuesday => "Tuesday",
Self::Wednesday => "Wednesday",
Self::Thursday => "Thursday",
Self::Friday => "Friday",
Self::Saturday => "Saturday",
Self::Sunday => "Sunday",
}
}
pub fn short_name(&self) -> &'static str {
match self {
Self::Monday => "Mon",
Self::Tuesday => "Tue",
Self::Wednesday => "Wed",
Self::Thursday => "Thu",
Self::Friday => "Fri",
Self::Saturday => "Sat",
Self::Sunday => "Sun",
}
}
}
impl TryFrom<i32> for Day {
type Error = TransformError;
@ -411,10 +476,18 @@ impl TryFrom<i32> for Day {
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Hash, Debug, Deref)]
#[derive(Serialize, Deserialize, Hash, Debug)]
#[serde(transparent)]
pub struct Days(Vec<Day>);
impl std::ops::Deref for Days {
type Target = Vec<Day>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(feature = "db")]
impl<'q> ::sqlx::encode::Encode<'q, sqlx::Postgres> for Days
where

View File

@ -1,16 +1,69 @@
pub struct I18n {}
mod pl;
use std::collections::HashMap;
pub struct I18n {
store: HashMap<String, HashMap<String, &'static str>>,
lang: String,
}
impl I18n {
pub fn load() -> Self {
// let languages: js_sys::Array = seed::window().navigator().languages();
// for lang in languages {
// let l: wasm_bindgen::JsValue = lang;
// if let Some(s) = l.as_string() {
// if let Ok(local) = pure_rust_locales::Locale::try_from(&s) {
// //
// }
// }
// }
Self {}
let mut i18n = Self {
store: HashMap::with_capacity(1_000),
lang: "".into(),
};
pl::define(&mut i18n);
let languages: js_sys::Array = seed::window().navigator().languages();
languages.find(&mut |lang, _idx, _array| {
let l: wasm_bindgen::JsValue = lang;
if let Some(s) = l.as_string() {
if i18n.store.contains_key(&s) {
i18n.lang = s;
true
} else {
false
}
} else {
false
}
});
i18n
}
fn scope(&mut self, lang: &'static str) -> Scope {
Scope { store: self, lang }
}
pub fn t(&self, key: &'static str) -> &'static str {
self.store
.get(self.lang.as_str())
.and_then(|s| s.get(key))
.copied()
.unwrap_or(key)
}
pub fn l(&self, key: &String) -> String {
self.store
.get(self.lang.as_str())
.and_then(|s| s.get(key))
.copied()
.map(Into::into)
.unwrap_or_else(|| key.clone())
}
}
pub struct Scope<'store, 'lang> {
lang: &'lang str,
store: &'store mut I18n,
}
impl<'store, 'lang> Scope<'store, 'lang> {
fn define(&mut self, key: &str, value: &'static str) -> &mut Self {
self.store
.store
.entry(self.lang.into())
.or_insert_with(|| HashMap::with_capacity(1_000))
.insert(key.into(), value);
self
}
}

24
web/src/i18n/pl.rs Normal file
View File

@ -0,0 +1,24 @@
use crate::i18n::I18n;
pub fn define(i18n: &mut I18n) {
i18n.scope("pl")
.define("Mon", "Pon")
.define("Tue", "Wt")
.define("Wed", "Śr")
.define("Thu", "Czw")
.define("Fri", "Pt")
.define("Sat", "Sob")
.define("Sun", "Ndz")
.define("Add to Cart", "Dodaj")
.define("Qty", "Ilość")
.define("Delivery every", "Dostawa w każdy")
.define("Cameras", "Kamery")
.define("Drugstore", "Drogeria")
.define("Speakers", "Głośniki")
.define("Phones", "Telefony")
.define("Sweets", "Słodycze")
.define("Memory", "Pamięć")
.define("Pants", "Spodnie")
.define("Clothes", "Ubrania")
.define("Plates", "Talerze");
}

View File

@ -7,6 +7,7 @@ pub mod shared;
use seed::empty;
use seed::prelude::*;
use crate::i18n::I18n;
use crate::model::Model;
use crate::pages::{Msg, Page, PublicPage};
@ -75,6 +76,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
.flatten()
.and_then(|el: web_sys::Element| el.get_attribute("href")),
shared: shared::Model::default(),
i18n: I18n::load(),
}
}

View File

@ -1,6 +1,6 @@
use seed::Url;
use crate::Page;
use crate::{I18n, Page};
pub struct Model {
pub url: Url,
@ -8,4 +8,5 @@ pub struct Model {
pub page: Page,
pub logo: Option<String>,
pub shared: crate::shared::Model,
pub i18n: I18n,
}

View File

@ -12,13 +12,13 @@ pub mod layout {
use seed::*;
pub fn view<Msg>(
url: Url,
model: &crate::Model,
content: Node<Msg>,
categories: Option<&[model::api::Category]>,
) -> Node<Msg> {
let sidebar = match categories {
Some(categories) => {
let sidebar = super::sidebar::view(url, categories);
let sidebar = super::sidebar::view(model, categories);
div![
C!["flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto border-r"],
sidebar
@ -40,10 +40,8 @@ pub mod sidebar {
use crate::pages::Urls;
pub fn view<Msg>(url: Url, categories: &[model::api::Category]) -> Node<Msg> {
let categories = categories
.iter()
.map(|category| item(url.clone(), category));
pub fn view<Msg>(model: &crate::Model, categories: &[model::api::Category]) -> Node<Msg> {
let categories = categories.iter().map(|category| item(model, category));
div![
C!["flex flex-col justify-between mt-6"],
@ -51,8 +49,8 @@ pub mod sidebar {
]
}
fn item<Msg>(url: Url, category: &model::api::Category) -> Node<Msg> {
let url = Urls::new(url)
fn item<Msg>(model: &crate::Model, category: &model::api::Category) -> Node<Msg> {
let url = Urls::new(model.url.clone())
.listing()
.add_path_part(category.key.as_str());
li![
@ -64,7 +62,7 @@ pub mod sidebar {
C!["w-6 h-6"],
attrs!["src" => category.svg.as_str(), "style" => ""]
],
span![C!["mx-4 font-medium"], category.name.as_str()]
span![C!["mx-4 font-medium"], model.i18n.l(&category.name)]
]
]
}

View File

@ -125,7 +125,7 @@ pub fn view(model: &crate::Model, page: &ListingPage) -> Node<crate::Msg> {
div![
crate::shared::view::public_navbar(model),
super::layout::view(model.url.clone(), content, Some(&page.categories))
super::layout::view(&model, content, Some(&page.categories))
]
}
@ -173,7 +173,10 @@ fn product(model: &crate::Model, 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"],
model.i18n.t("Add to Cart")
],
div![C!["mt-1 text-xl font-semibold"], price],
]
]

View File

@ -4,12 +4,14 @@ use seed::*;
#[derive(Debug)]
pub enum Msg {
ProductFetched(fetch::Result<model::api::Product>),
SelectImage(usize),
}
#[derive(Debug, Default)]
pub struct ProductPage {
pub product_id: Option<model::ProductId>,
pub product: Option<model::api::Product>,
pub selected_image: usize,
}
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>) -> ProductPage {
@ -23,6 +25,7 @@ pub fn init(mut url: Url, orders: &mut impl Orders<Msg>) -> ProductPage {
ProductPage {
product_id: Some(product_id),
product: None,
selected_image: 0,
}
}
@ -36,6 +39,9 @@ pub fn update(msg: Msg, model: &mut ProductPage, _orders: &mut impl Orders<Msg>)
Msg::ProductFetched(Err(e)) => {
seed::error!(e);
}
Msg::SelectImage(selected) => {
model.selected_image = selected;
}
}
}
@ -44,11 +50,23 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
None => return empty!(),
Some(product) => product,
};
let images = product
let large_photo = product
.photos
.get(page.selected_image)
.map(image)
.unwrap_or_else(|| empty![]);
let small_photos = {
if product.photos.len() <= 1 {
empty![]
} else {
let photos = product
.photos
.iter()
.enumerate()
.map(|(idx, img)| image(idx, img));
.map(|(idx, img)| small_image(idx, page.selected_image, img));
div![C!["flex -mx-2 mb-4"], photos]
}
};
let description = product
.long_description
@ -56,16 +74,20 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
.split('\n')
.map(|s| div![s]);
let delivery = delivery_available(product, model);
let content = div![
C!["max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6"],
attrs!["id" => "full-window"],
div![
C!["flex flex-col md:flex-row -mx-4"],
attrs!["id" => "product-header"],
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!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4"], large_photo]
],
small_photos
],
div![
C!["md:flex-1 px-4"], attrs!["id" => "details"],
@ -73,22 +95,82 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"],
product.name.as_str()
],
div![description]
div![
delivery
],
div![
C!["flex py-4 space-x-4"],
div![
C!["relative"],
label![
C!["text-center left-0 pt-2 right-0 absolute block text-xs uppercase text-gray-400 tracking-wide font-semibold"],
attrs!["for" => "quantity"],
model.i18n.t("Qty")
],
input![C![""], attrs!["id" => "quantity", "type" => "number"]]
],
button![
C!["px-6 py-3 text-sm text-white bg-indigo-500 rounded-lg outline-none hover:bg-indigo-600 ring-indigo-300"],
model.i18n.t("Add to Cart"),
ev("click", move |ev| {
ev.prevent_default();
ev.stop_propagation();
None as Option<Msg>
})
]
]
],
],
div![
C!["-mx-4"],
attrs!["id" => "product-header"],
div![description]
]
].map_msg(map_to_global);
div![
crate::shared::view::public_navbar(model),
super::layout::view(model.url.clone(), content, None)
super::layout::view(model, content, None)
]
}
fn image(idx: usize, img: &model::api::Photo) -> Node<Msg> {
fn delivery_available(product: &model::api::Product, model: &crate::Model) -> Node<Msg> {
let days = product
.deliver_days_flag
.iter()
.map(|day| div![
C!["focus:outline-none w-14 md:w-18 rounded-lg h-14 md:h-18 bg-gray-100 flex items-center justify-center ring-indigo-300 ring-inset"],
model.i18n.t(day.short_name())
]);
div![
div![C![""], model.i18n.t("Delivery every")],
div![C!["flex py-4 space-x-4"], days]
]
}
fn small_image(idx: usize, selected: usize, img: &model::api::Photo) -> Node<Msg> {
div![
C!["flex-1 px-2"],
button![
C!["focus:outline-none w-full rounded-lg h-24 md:h-32 bg-gray-100 flex items-center justify-center ring-indigo-300 ring-inset"],
IF![selected == idx => C!["ring-2"]],
img![
C!["h-24 md:h-32"],
attrs!["src" => img.url.as_str()]
],
ev("click", move |ev| {
ev.prevent_default();
ev.stop_propagation();
Msg::SelectImage(idx)
})
]
]
}
fn image(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()]]
img![C!["h-64 md:h-80"], attrs!["src" => img.url.as_str()]]
]
}