diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index 8a315cc..b539f5d 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -153,7 +153,7 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Hash)] pub struct Photo { pub id: crate::PhotoId, pub file_name: crate::FileName, @@ -162,7 +162,7 @@ pub struct Photo { } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Hash)] pub struct Product { pub id: crate::ProductId, pub name: crate::ProductName, diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index f03f345..b94b97e 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -19,7 +19,7 @@ use serde::{Deserialize, Deserializer, Serialize}; pub use crate::encrypt::*; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Hash, thiserror::Error)] pub enum TransformError { #[error("Given value is below minimal value")] BelowMinimal, @@ -34,7 +34,7 @@ pub type RecordId = i32; #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum OrderStatus { #[display(fmt = "Potwierdzone")] @@ -54,7 +54,7 @@ pub enum OrderStatus { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)] +#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum Role { #[display(fmt = "Adminitrator")] @@ -80,7 +80,7 @@ impl Role { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] -#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum QuantityUnit { #[cfg_attr(feature = "db", sqlx(rename = "g"))] @@ -96,7 +96,7 @@ pub enum QuantityUnit { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum PaymentMethod { PayU, @@ -106,7 +106,7 @@ pub enum PaymentMethod { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ShoppingCartState { Active, @@ -116,7 +116,7 @@ pub enum ShoppingCartState { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Display, Deserialize, Serialize, PartialEq)] +#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum Audience { Web, @@ -161,14 +161,14 @@ impl Default for Audience { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)] +#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Hash, Deref, From)] #[serde(transparent)] pub struct Price(NonNegative); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Deref, From)] +#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Hash, Deref, From)] #[serde(transparent)] pub struct Quantity(NonNegative); @@ -241,7 +241,7 @@ impl<'de> serde::Deserialize<'de> for Email { #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Default, Debug, Copy, Clone, Deref, Display)] +#[derive(Serialize, Default, Debug, Copy, Clone, Hash, Deref, Display)] #[serde(transparent)] pub struct NonNegative(i32); @@ -323,7 +323,7 @@ impl<'de> serde::Deserialize<'de> for NonNegative { } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Display, From)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, Display, From)] #[serde(rename_all = "lowercase")] pub enum Day { Monday = 1 << 0, @@ -360,7 +360,7 @@ impl TryFrom for Day { } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] -#[derive(Serialize, Deserialize, Deref, Debug)] +#[derive(Serialize, Deserialize, Hash, Debug, Deref)] #[serde(transparent)] pub struct Days(Vec); @@ -536,26 +536,26 @@ pub struct ProductId(RecordId); #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] #[serde(transparent)] pub struct ProductName(String); #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] #[serde(transparent)] pub struct ProductShortDesc(String); #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] #[serde(transparent)] pub struct ProductLongDesc(String); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Clone, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash, Deref, Display, From)] #[serde(transparent)] pub struct ProductCategory(String); @@ -567,7 +567,7 @@ impl ProductCategory { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Hash)] pub struct Product { pub id: ProductId, pub name: ProductName, @@ -780,7 +780,7 @@ impl RefreshTokenString { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] pub struct LocalPath(String); impl LocalPath { @@ -792,7 +792,7 @@ impl LocalPath { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] pub struct UniqueName(String); impl UniqueName { @@ -804,7 +804,7 @@ impl UniqueName { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] pub struct FileName(String); impl FileName { @@ -816,18 +816,18 @@ impl FileName { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, Deref, Display, From)] pub struct PhotoId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Serialize, Deserialize, Debug, Deref, Display, From)] +#[derive(Serialize, Deserialize, Debug, Hash, Deref, Display, From)] pub struct ProductPhotoId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Hash)] pub struct Photo { pub id: PhotoId, pub local_path: LocalPath, @@ -837,7 +837,7 @@ pub struct Photo { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Hash)] pub struct ProductLinkedPhoto { pub id: PhotoId, pub local_path: LocalPath, diff --git a/web/src/lib.rs b/web/src/lib.rs index 6fc61c2..cb133e2 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -24,7 +24,9 @@ macro_rules! fetch_page { } fn init(url: Url, orders: &mut impl Orders) -> Model { - orders.stream(streams::interval(500, || Msg::CheckAccessToken)); + orders + .stream(streams::interval(500, || Msg::CheckAccessToken)) + .subscribe(Msg::UrlChanged); Model { token: LocalStorage::get("auth-token").ok(), diff --git a/web/src/pages.rs b/web/src/pages.rs index a2d516d..3436618 100644 --- a/web/src/pages.rs +++ b/web/src/pages.rs @@ -40,6 +40,13 @@ impl Page { 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))), + ))) + } ["admin"] => Self::Admin(AdminPage::Landing), _ => Self::Public(PublicPage::Listing(public::listing::init( url, @@ -62,7 +69,7 @@ impl<'a> Urls<'a> { // Public fn listing(self) -> Url { - self.base_url().add_path_part("listing") + self.base_url().add_path_part("products") } fn product(self) -> Url { diff --git a/web/src/pages/public.rs b/web/src/pages/public.rs index 8b893ea..719cc66 100644 --- a/web/src/pages/public.rs +++ b/web/src/pages/public.rs @@ -4,3 +4,49 @@ pub mod listing; pub enum Msg { Listing(listing::Msg), } + +pub mod layout { + use seed::prelude::*; + use seed::*; + + pub fn view(url: Url, content: Node, categories: &[String]) -> Node { + div![ + C!["flex"], + div![ + C!["flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto border-r"], + super::sidebar::view(url, categories) + ], + div![C!["w-full h-full p-4 m-8 overflow-y-auto"], content] + ] + } +} + +pub mod sidebar { + use seed::prelude::*; + use seed::*; + + use crate::pages::Urls; + + pub fn view(url: Url, categories: &[String]) -> Node { + let categories = categories + .iter() + .map(|category| item(url.clone(), category.as_str())); + + div![ + C!["flex flex-col justify-between mt-6"], + aside![ul![categories]] + ] + } + + fn item(url: Url, category: &str) -> Node { + let url = Urls::new(url).listing().add_path_part(category); + 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] + ] + ] + } +} diff --git a/web/src/pages/public/listing.rs b/web/src/pages/public/listing.rs index 6930814..b9dfa14 100644 --- a/web/src/pages/public/listing.rs +++ b/web/src/pages/public/listing.rs @@ -1,26 +1,49 @@ +use std::collections::{HashMap, HashSet}; + use seed::app::Orders; use seed::prelude::*; use seed::*; #[derive(Debug)] pub struct Model { - pub products: Vec, + url: Url, + pub products: HashMap, pub errors: Vec, + pub categories: Vec, + pub filters: HashSet, + pub visible_products: Vec, } #[derive(Debug)] pub enum Msg { FetchProducts, ProductFetched(fetch::Result), - Shared(crate::shared::Msg) + Shared(crate::shared::Msg), } -pub fn init(_url: Url, orders: &mut impl Orders) -> Model { +pub fn init(url: Url, orders: &mut impl Orders) -> 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(), + }; orders.send_msg(Msg::FetchProducts); - Model { - products: vec![], + let model = Model { + url: url.to_base_url(), + 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) { @@ -31,7 +54,38 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { }); } Msg::ProductFetched(Ok(products)) => { - model.products = products.0; + model.categories = products + .0 + .iter() + .fold(HashSet::new(), |mut set, p| { + if let Some(category) = p.category.as_deref() { + set.insert(String::from(category)); + } + set + }) + .into_iter() + .collect(); + model.categories.sort(); + model.products = { + let len = products.0.len(); + products + .0 + .into_iter() + .fold(HashMap::with_capacity(len), |mut m, p| { + m.insert(p.id, p); + m + }) + }; + model.visible_products = model + .products + .iter() + .filter_map(|(_, p)| { + p.category + .as_deref() + .filter(|c| model.filters.contains(*c)) + .map(|_| p.id) + }) + .collect(); } Msg::ProductFetched(Err(_e)) => { model.errors.push("Failed to load products".into()); @@ -41,14 +95,22 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } pub fn view(model: &crate::Model, page: &Model) -> Node { - let products = page.products.iter().map(product); + let products = page + .visible_products + .iter() + .filter_map(|id| page.products.get(id)) + .map(product); div![ crate::shared::view::public_navbar(model), - div![ - C!["grid grid-cols-1 gap-4 lg:grid-cols-6 sm:grid-cols-2"], - products - ] + 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 + ) ] }