More images, translations, product page
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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()),
|
||||
})
|
||||
}
|
||||
|
@ -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
@ -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 |
@ -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(())
|
||||
};
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 714 KiB |
After Width: | Height: | Size: 196 KiB |
After Width: | Height: | Size: 586 KiB |
BIN
assets/examples/backup-images/pexels-alex-azabache-3907507.webp
Normal file
After Width: | Height: | Size: 523 KiB |
BIN
assets/examples/backup-images/pexels-binoid-cbd-3612182.webp
Normal file
After Width: | Height: | Size: 331 KiB |
BIN
assets/examples/backup-images/pexels-caio-1279107.webp
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/examples/backup-images/pexels-eprism-studio-335257.webp
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
assets/examples/backup-images/pexels-gabriel-freytez-341523.webp
Normal file
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 136 KiB |
BIN
assets/examples/backup-images/pexels-luis-quintero-1738641.webp
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/examples/backup-images/pexels-math-90946.webp
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
assets/examples/backup-images/pexels-mike-380954.webp
Normal file
After Width: | Height: | Size: 373 KiB |
BIN
assets/examples/backup-images/pexels-pixabay-279906.webp
Normal file
After Width: | Height: | Size: 163 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 31 KiB |
BIN
assets/examples/images/pexels-agnese-lunecka-10322857.webp
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
assets/examples/images/pexels-agnese-lunecka-11179383.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
assets/examples/images/pexels-agnese-lunecka-11328773.webp
Normal file
After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 523 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 373 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 32 KiB |
@ -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 = [] }
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
@ -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");
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -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],
|
||||
]
|
||||
]
|
||||
|
@ -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()]]
|
||||
]
|
||||
}
|
||||
|
||||
|