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

View File

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

View File

@ -157,7 +157,7 @@ pub struct WriteResult {
#[rtype(result = "Result<WriteResult>")] #[rtype(result = "Result<WriteResult>")]
pub struct WriteFile { pub struct WriteFile {
pub file_name: String, 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); 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 { if counter % 100_000 == 0 {
log::debug!("Wrote {} for {:?}", counter, file_name); log::debug!("Wrote {} for {:?}", counter, file_name);
} }
match file.write(&[b]) { match file.write(&b) {
Ok(_) => {} Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::StorageFull => return Err(Error::NoSpace), Err(e) if e.kind() == std::io::ErrorKind::StorageFull => return Err(Error::NoSpace),
Err(e) => return Err(Error::CantWrite(e)), Err(e) => return Err(Error::CantWrite(e)),
@ -221,6 +221,6 @@ pub(crate) async fn write_file(msg: WriteFile, config: SharedAppConfig) -> Resul
Ok(WriteResult { Ok(WriteResult {
file_name: FileName::new(file_name), file_name: FileName::new(file_name),
unique_name: UniqueName::new(unique_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" } human-panic = { version = "1.0.3" }
bytes = { version = "1.1.0" }
actix = { version = "0.13", features = [] } actix = { version = "0.13", features = [] }
actix-rt = { version = "2.7", features = [] } actix-rt = { version = "2.7", features = [] }
actix-web = { version = "4.0", 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 { let read = async {
while let Some(Ok(data)) = field.next().await { while let Some(Ok(data)) = field.next().await {
for b in data { if let Err(e) = tx.send(data) {
if let Err(e) = tx.send(b) {
log::error!("{e:?}"); log::error!("{e:?}");
return Err(UploadError::InvalidName); return Err(UploadError::InvalidName);
} }
} }
}
Ok(()) Ok(())
}; };

View File

@ -84,6 +84,7 @@ async fn svg(path: Path<String>) -> HttpResponse {
"sweets" => serve_svg!("sweets"), "sweets" => serve_svg!("sweets"),
"vegetables" => serve_svg!("vegetables"), "vegetables" => serve_svg!("vegetables"),
"memory" => serve_svg!("memory"), "memory" => serve_svg!("memory"),
"plates" => serve_svg!("plate"),
_ => HttpResponse::NotFound().finish(), _ => 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"] } database_manager = { path = "../actors/database_manager", features = ["dummy"] }
fs_manager = { path = "../actors/fs_manager", features = [] } fs_manager = { path = "../actors/fs_manager", features = [] }
bytes = { version = "1.1.0" }
actix = { version = "0.13", features = [] } actix = { version = "0.13", features = [] }
actix-rt = { version = "2.7", features = [] } actix-rt = { version = "2.7", features = [] }
actix-web = { version = "4.0", features = [] } actix-web = { version = "4.0", features = [] }

View File

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

View File

@ -122,7 +122,25 @@ pub(crate) async fn create_product_photos(
seed.clone(), seed.clone(),
"Venus HD Professional", "Venus HD Professional",
"pexels-Venus-HD-Make-up-and-perfume-2587370.webp" "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.0.unwrap();
results.1.unwrap(); results.1.unwrap();

View File

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

View File

@ -39,46 +39,86 @@ pub struct Category {
pub svg: &'static str, 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 { Category {
name: "Cameras", name: Category::CAMERAS_NAME,
key: "cameras", key: Category::CAMERAS_KEY,
svg: "/svg/cameras.svg", svg: category_svg!("cameras"),
}, },
Category { Category {
name: "Drugstore", name: Category::DRUGSTORE_NAME,
key: "drugstore", key: Category::DRUGSTORE_KEY,
svg: "/svg/drugstore.svg", svg: category_svg!("drugstore"),
}, },
Category { Category {
name: "Speakers", name: Category::SPEAKERS_NAME,
key: "speakers", key: Category::SPEAKERS_KEY,
svg: "/svg/speakers.svg", svg: category_svg!("speakers"),
}, },
Category { Category {
name: "Phones", name: Category::PHONES_NAME,
key: "phones", key: Category::PHONES_KEY,
svg: "/svg/phones.svg", svg: category_svg!("phones"),
}, },
Category { Category {
name: "Sweets", name: Category::SWEETS_NAME,
key: "sweets", key: Category::SWEETS_KEY,
svg: "/svg/sweets.svg", svg: category_svg!("sweets"),
}, },
Category { Category {
name: "Memory", name: Category::MEMORY_NAME,
key: "memory", key: Category::MEMORY_KEY,
svg: "/svg/memory.svg", svg: category_svg!("memory"),
}, },
Category { Category {
name: "Pants", name: Category::PANTS_NAME,
key: "pants", key: Category::PANTS_KEY,
svg: "/svg/pants.svg", svg: category_svg!("pants"),
}, },
Category { Category {
name: "Clothes", name: Category::CLOTHES_NAME,
key: "clothes", key: Category::CLOTHES_KEY,
svg: "/svg/clothes.svg", 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, 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 { impl TryFrom<i32> for Day {
type Error = TransformError; type Error = TransformError;
@ -411,10 +476,18 @@ impl TryFrom<i32> for Day {
} }
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Hash, Debug, Deref)] #[derive(Serialize, Deserialize, Hash, Debug)]
#[serde(transparent)] #[serde(transparent)]
pub struct Days(Vec<Day>); 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")] #[cfg(feature = "db")]
impl<'q> ::sqlx::encode::Encode<'q, sqlx::Postgres> for Days impl<'q> ::sqlx::encode::Encode<'q, sqlx::Postgres> for Days
where 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 { impl I18n {
pub fn load() -> Self { pub fn load() -> Self {
// let languages: js_sys::Array = seed::window().navigator().languages(); let mut i18n = Self {
// for lang in languages { store: HashMap::with_capacity(1_000),
// let l: wasm_bindgen::JsValue = lang; lang: "".into(),
// if let Some(s) = l.as_string() { };
// if let Ok(local) = pure_rust_locales::Locale::try_from(&s) { 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() {
Self {} 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::empty;
use seed::prelude::*; use seed::prelude::*;
use crate::i18n::I18n;
use crate::model::Model; use crate::model::Model;
use crate::pages::{Msg, Page, PublicPage}; use crate::pages::{Msg, Page, PublicPage};
@ -75,6 +76,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
.flatten() .flatten()
.and_then(|el: web_sys::Element| el.get_attribute("href")), .and_then(|el: web_sys::Element| el.get_attribute("href")),
shared: shared::Model::default(), shared: shared::Model::default(),
i18n: I18n::load(),
} }
} }

View File

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

View File

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

View File

@ -4,12 +4,14 @@ use seed::*;
#[derive(Debug)] #[derive(Debug)]
pub enum Msg { pub enum Msg {
ProductFetched(fetch::Result<model::api::Product>), ProductFetched(fetch::Result<model::api::Product>),
SelectImage(usize),
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ProductPage { pub struct ProductPage {
pub product_id: Option<model::ProductId>, pub product_id: Option<model::ProductId>,
pub product: Option<model::api::Product>, pub product: Option<model::api::Product>,
pub selected_image: usize,
} }
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>) -> ProductPage { 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 { ProductPage {
product_id: Some(product_id), product_id: Some(product_id),
product: None, 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)) => { Msg::ProductFetched(Err(e)) => {
seed::error!(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!(), None => return empty!(),
Some(product) => product, 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 .photos
.iter() .iter()
.enumerate() .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 let description = product
.long_description .long_description
@ -56,16 +74,20 @@ pub fn view(model: &crate::Model, page: &ProductPage) -> Node<crate::Msg> {
.split('\n') .split('\n')
.map(|s| div![s]); .map(|s| div![s]);
let delivery = delivery_available(product, model);
let content = div![ let content = div![
C!["max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6"], C!["max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6"],
attrs!["id" => "full-window"],
div![ div![
C!["flex flex-col md:flex-row -mx-4"], C!["flex flex-col md:flex-row -mx-4"],
attrs!["id" => "product-header"],
div![ div![
C!["md:flex-1 px-4"], attrs!["id" => "photos"], C!["md:flex-1 px-4"], attrs!["id" => "photos"],
div![ div![
attrs!["x-data" => "{ image: 1}"], div![C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4"], large_photo]
div![C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4"], images] ],
] small_photos
], ],
div![ div![
C!["md:flex-1 px-4"], attrs!["id" => "details"], 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"], C!["mb-2 leading-tight tracking-tight font-bold text-gray-800 text-2xl md:text-3xl"],
product.name.as_str() 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); ].map_msg(map_to_global);
div![ div![
crate::shared::view::public_navbar(model), 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![ div![
C!["h-64 md:h-80 rounded-lg bg-gray-100 mb-4 flex items-center justify-center"], 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![C!["h-64 md:h-80"], attrs!["src" => img.url.as_str()]]
img![attrs!["src" => img.url.as_str()]]
] ]
} }