diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/mod.rs | 50 | ||||
| -rw-r--r-- | src/components/async_fetch.rs | 50 | ||||
| -rw-r--r-- | src/components/container.rs | 47 | ||||
| -rw-r--r-- | src/components/icon_p.rs | 18 | ||||
| -rw-r--r-- | src/components/inventory.rs | 55 | ||||
| -rw-r--r-- | src/components/mod.rs | 6 | ||||
| -rw-r--r-- | src/components/product_overview.rs | 71 | ||||
| -rw-r--r-- | src/components/recipies.rs | 12 | ||||
| -rw-r--r-- | src/components/side_header.rs | 22 | ||||
| -rw-r--r-- | src/components/site_header.rs | 29 | ||||
| -rw-r--r-- | src/lib.rs | 41 | ||||
| -rw-r--r-- | src/main.rs | 5 | ||||
| -rw-r--r-- | src/pages/home.rs | 24 |
13 files changed, 321 insertions, 109 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..8b9e77d --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,50 @@ +use leptos::{ + error::Error, + prelude::{Read, expect_context}, +}; +use reactive_stores::Store; +use rocie_client::{ + apis::{ + api_get_inventory_api::amount_by_id, + api_get_product_api::{product_by_id, products}, + api_get_unit_api::unit_by_id, + }, + models::{Product, ProductAmount, ProductId, Unit, UnitId}, +}; + +use crate::{ConfigState, ConfigStateStoreFields}; + +pub(crate) async fn get_amount_by_id(product_id: ProductId) -> Result<ProductAmount, Error> { + let config = expect_context::<Store<ConfigState>>(); + amount_by_id(&config.config().read(), product_id) + .await + .map_err(Into::<Error>::into) +} +pub(crate) async fn get_product_by_id(product_id: ProductId) -> Result<Product, Error> { + let config = expect_context::<Store<ConfigState>>(); + product_by_id(&config.config().read(), product_id) + .await + .map_err(Into::<Error>::into) +} +pub(crate) async fn get_unit_by_id(unit_id: UnitId) -> Result<Unit, Error> { + let config = expect_context::<Store<ConfigState>>(); + unit_by_id(&config.config().read(), unit_id) + .await + .map_err(Into::<Error>::into) +} +pub(crate) async fn get_full_product_by_id( + id: ProductId, +) -> Result<(Product, ProductAmount, Unit), Error> { + let amount = get_amount_by_id(id).await?; + let product = get_product_by_id(id).await?; + let unit = get_unit_by_id(amount.amount.unit).await?; + + Ok::<_, Error>((product, amount, unit)) +} + +pub(crate) async fn get_products() -> Result<Vec<Product>, Error> { + let config = expect_context::<Store<ConfigState>>(); + products(&config.config().read()) + .await + .map_err(Into::<Error>::into) +} diff --git a/src/components/async_fetch.rs b/src/components/async_fetch.rs new file mode 100644 index 0000000..7105c6f --- /dev/null +++ b/src/components/async_fetch.rs @@ -0,0 +1,50 @@ +macro_rules! AsyncFetch { + (fetcher = $fetcher:block producer = |$bound_variable:pat_param| $producer:block) => {{ + use leptos::{ + prelude::{ElementChild, LocalResource, Suspend, Transition}, + view, + }; + + view! { + <Transition fallback=|| { + view! { <p>"Loading..."</p> } + }> + {move || Suspend::new(async move { + let resource = { LocalResource::new(move || $fetcher) }; + resource + .await + .map(|$bound_variable| $producer) + })} + </Transition> + } + }}; +} +pub(crate) use AsyncFetch; + +// #[component] +// pub fn AsyncFetch<P, V, T, Fut>( +// fetcher: impl Fn() -> Fut + 'static + Send + Sync, +// producer: P, +// ) -> impl IntoView +// where +// V: IntoView + 'static, +// P: Fn(T) -> V + 'static + Send + Sync, +// Fut: Future<Output = Result<T, Error>> + 'static, +// T: 'static, +// LocalResource<Result<T, Error>>: IntoFuture<Output = Result<T, Error>> + Send, +// { +// view! { +// <Transition fallback=|| { +// view! { <p>"Loading..."</p> } +// }> +// { || Suspend::new(async { +// let value_resource = LocalResource::new( || fetcher()); +// value_resource +// .await +// .map(|value| { +// producer(value) +// }) +// })} +// </Transition> +// } +// } diff --git a/src/components/container.rs b/src/components/container.rs index cf7aa5a..7a4a64f 100644 --- a/src/components/container.rs +++ b/src/components/container.rs @@ -1,34 +1,35 @@ use leptos::{ IntoView, component, - prelude::{Children, ClassAttribute, ElementChild}, + prelude::{Children, ClassAttribute, ElementChild, OnAttribute}, view, }; -use leptos_meta::Style; +use leptos_router::{NavigateOptions, hooks::use_navigate}; #[component] -pub fn Container(header: impl IntoView, children: Children) -> impl IntoView { +pub fn Container( + header: impl IntoView, + buttons: Vec<(impl IntoView, &'static str)>, + children: Children, +) -> impl IntoView { view! { - <Style> - " - .rocie-container { - border-width: 0.1rem; - border-style: solid; - border-color: gray; - border-radius: 15%; - - padding: 0.2rem; - - text-align: left; - } - .rocie-container-header { - font-size: 1.5rem; - } - " - </Style> - - <div class="rocie-container"> - <p class="rocie-container-header">{header}</p> + <div class="p-4 mt-4 mr-4 ml-4 md-2 rounded-lg border-gray-600 border"> + <p class="text-lg text-bold">{header}</p> {children()} + + <ul class="flex flex-row gap-1 pt-2 overflow-x-auto"> + {buttons + .into_iter() + .map(|(name, path)| { + view! { + <li class="bg-green-400/40 p-2 first:rounded-l-full last:rounded-r-full"> + <button on:click=move |_| { + use_navigate()(path, NavigateOptions::default()); + }>{name}</button> + </li> + } + }) + .collect::<Vec<_>>()} + </ul> </div> } } diff --git a/src/components/icon_p.rs b/src/components/icon_p.rs new file mode 100644 index 0000000..372e280 --- /dev/null +++ b/src/components/icon_p.rs @@ -0,0 +1,18 @@ +use leptos::{ + IntoView, component, + prelude::{ClassAttribute, ElementChild, Signal}, + view, +}; +use leptos_icons::Icon; + +#[component] +pub fn IconP(#[prop(into)] icon: Signal<icondata_core::Icon>, text: &'static str) -> impl IntoView { + view! { + <div class="flex justify-evenly items-center"> + <div class="mr-1"> + <Icon icon=icon /> + </div> + <p>{text}</p> + </div> + } +} diff --git a/src/components/inventory.rs b/src/components/inventory.rs new file mode 100644 index 0000000..5855b33 --- /dev/null +++ b/src/components/inventory.rs @@ -0,0 +1,55 @@ +use leptos::{ + IntoView, component, + prelude::{ClassAttribute, ElementChild}, + view, +}; + +use crate::{ + api::{get_full_product_by_id, get_products}, + components::{async_fetch::AsyncFetch, site_header::SiteHeader}, +}; + +#[component] +pub fn Inventory() -> impl IntoView { + view! { + <SiteHeader logo=icondata_io::IoArrowBack back_location="/" name="Inventory" /> + + <ul class="flex flex-col p-2 m-2"> + { + AsyncFetch! { + fetcher = {get_products()} + producer = |products| { + products + .into_iter() + .map(|product| { + view! { + {AsyncFetch! { + fetcher = {get_full_product_by_id(product.id)} + producer = |(product, amount, unit)| { + view! { + <ul class="my-3"> + <li class="m-2">{product.name}</li> + <li class="m-2"> + <span class="bg-gray-200 p-1 px-2 rounded-lg"> + { + format!( + "{} {}", + amount.amount.value, + unit.short_name + ) + } + </span> + </li> + </ul> + } + } + }} + } + }) + .collect::<Vec<_>>() + } + } + } + </ul> + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 85f9671..7174ad8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,6 +1,10 @@ // Generic +pub mod async_fetch; pub mod container; +pub mod icon_p; // Specific +pub mod inventory; pub mod product_overview; -pub mod side_header; +pub mod recipies; +pub mod site_header; diff --git a/src/components/product_overview.rs b/src/components/product_overview.rs index 4e95335..ae2eaf2 100644 --- a/src/components/product_overview.rs +++ b/src/components/product_overview.rs @@ -1,47 +1,40 @@ -use std::sync::Arc; +use leptos::{IntoView, component, view}; -use leptos::{ - IntoView, component, - prelude::{ElementChild, Set, signal}, - task::spawn_local, - view, +use crate::{ + api::get_products, + components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP}, }; -use rocie_client::apis::{api_get_product_api::products, configuration::Configuration}; - -use crate::components::container::Container; #[component] -pub fn ProductOverview(config: Arc<Configuration>) -> impl IntoView { - let (read_status, write_status) = signal("Loading..".to_owned()); - - { - let local_config = Arc::clone(&config); - - spawn_local(async move { - let products = products(&local_config).await; - - write_status.set( - products - .as_ref() - .map(move |products| { - let products_num = products.len(); - let plural_s = if products_num == 1 { "" } else { "s" }; - let products_value = 2; - let products_currency = "EUR"; - format!( - "You have {products_num} product{plural_s} \ - in stock with a value \ - of {products_value} {products_currency}.", - ) - }) - .unwrap(), - ); - }); - } - +pub fn ProductOverview() -> impl IntoView { view! { - <Container header="Inventory"> - <p>{read_status}</p> + <Container + header="Inventory" + buttons=vec![ + (view! { <IconP icon=icondata_io::IoClipboardOutline text="Inventory" /> }, "inventory"), + (view! { <IconP icon=icondata_io::IoPricetags text="Consume" /> }, "consume"), + (view! { <IconP icon=icondata_io::IoStorefront text="Buy" /> }, "buy"), + ] + > + {AsyncFetch!( + fetcher = {get_products()} + producer = |products| { + let products_num = products.len(); + let plural_s = if products_num == 1 { "" } else { "s" }; + let products_value = 2; + let products_currency = "EUR"; + + view! { + <p> + {format!( + "You have {products_num} product{plural_s} \ + in stock with a value \ + of {products_value} {products_currency}.", + )} + </p> + } + } + )} </Container> } } diff --git a/src/components/recipies.rs b/src/components/recipies.rs new file mode 100644 index 0000000..1bd3a0d --- /dev/null +++ b/src/components/recipies.rs @@ -0,0 +1,12 @@ +use leptos::{IntoView, component, prelude::ElementChild, view}; + +use crate::components::container::Container; + +#[component] +pub fn Recipies() -> impl IntoView { + view! { + <Container header="Recipies" buttons=vec![("Mealplan", "mealplan")]> + <p>"You have 0 recipies."</p> + </Container> + } +} diff --git a/src/components/side_header.rs b/src/components/side_header.rs deleted file mode 100644 index 9cd6777..0000000 --- a/src/components/side_header.rs +++ /dev/null @@ -1,22 +0,0 @@ -use leptos::prelude::{AddAnyAttr, ElementChild, IntoView, StyleAttribute, component, view}; -use leptos_router::{NavigateOptions, hooks::use_navigate}; -use thaw::{Flex, FlexJustify, LayoutHeader}; - -#[component] -pub fn SiteHeader() -> impl IntoView { - let navigate = use_navigate(); - - view! { - <LayoutHeader - attr:style="padding: 20px;" - on:click=move |_| { - navigate("/", NavigateOptions::default()); - } - > - <Flex justify=FlexJustify::SpaceAround> - <img src="/logo.svg" style="width: 36px" /> - <h3>"Rocie"</h3> - </Flex> - </LayoutHeader> - } -} diff --git a/src/components/site_header.rs b/src/components/site_header.rs new file mode 100644 index 0000000..65f7137 --- /dev/null +++ b/src/components/site_header.rs @@ -0,0 +1,29 @@ +use icondata_core::Icon as DataIcon; +use leptos::prelude::{ClassAttribute, ElementChild, IntoView, OnAttribute, component, view}; +use leptos_icons::Icon; +use leptos_router::{NavigateOptions, hooks::use_navigate}; + +#[component] +pub fn SiteHeader( + logo: DataIcon, + back_location: &'static str, + name: &'static str, + #[prop(default = None)] menu: Option<String>, +) -> impl IntoView { + let navigate = use_navigate(); + + view! { + <div + class="flex flex-row justify-between items-center p-2 px-4 w-full h-1/12 bg-gray-100" + on:click=move |_| { + navigate(back_location, NavigateOptions::default()); + } + > + <div class="max-w-14"> + <Icon icon=logo /> + </div> + <p class="text-xl">{name}</p> + <div>{menu}</div> + </div> + } +} diff --git a/src/lib.rs b/src/lib.rs index 357ce93..a488d95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,24 +7,38 @@ reason = "Can't add it to leptos' components" )] #![expect( - clippy::needless_pass_by_value, - reason = "Can't add it to leptos' components" + unused_extern_crates, + reason = "Deependency needed to inject the `js` feature into uuid" )] +extern crate uuid; + +// All of them are only used in the `main.rs` and not in the `lib.rs` part of this crate. +extern crate console_error_panic_hook; +extern crate console_log; +extern crate log; +mod api; mod components; mod pages; -use std::sync::Arc; - -use leptos::prelude::{AddAnyAttr, IntoView, component, view}; +use leptos::prelude::{AddAnyAttr, IntoView, component, provide_context, view}; use leptos_meta::{Html, Meta, Title, provide_meta_context}; use leptos_router::{ components::{Route, Router, Routes}, path, }; +use reactive_stores::Store; use rocie_client::apis::configuration::Configuration; -use crate::pages::{home::Home, not_found::NotFound}; +use crate::{ + components::inventory::Inventory, + pages::{home::Home, not_found::NotFound}, +}; + +#[derive(Debug, Clone, Store)] +pub struct ConfigState { + config: Configuration, +} #[component] pub fn App() -> impl IntoView { @@ -37,13 +51,15 @@ pub fn App() -> impl IntoView { config.user_agent = Some("rocie-mobile".to_owned()); "http://127.0.0.1:8080".clone_into(&mut config.base_path); - Arc::new(config) + config }; + provide_context(Store::new(ConfigState { config })); + view! { <Html attr:lang="en" attr:dir="ltr" attr:data-theme="light" /> - <Title text="Welcome to Leptos CSR" /> + <Title text="Rocie-mobile" /> <Meta charset="UTF-8" /> <Meta name="viewport" content="width=device-width, initial-scale=1.0" /> @@ -53,8 +69,13 @@ pub fn App() -> impl IntoView { <Route path=path!("/") view=move || { - let local_config = Arc::clone(&config); - view! { <Home config=local_config /> } + view! { <Home /> } + } + /> + <Route + path=path!("/inventory") + view=move || { + view! { <Inventory /> } } /> </Routes> diff --git a/src/main.rs b/src/main.rs index c377484..c3eaadf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,8 @@ +#![expect( + unused_crate_dependencies, + reason = "We use them in the lib version of this crate" +)] + use leptos::prelude::{mount_to_body, view}; use rocie_mobile::App; diff --git a/src/pages/home.rs b/src/pages/home.rs index 8749860..387562e 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -1,26 +1,23 @@ -use std::sync::Arc; - use leptos::{ IntoView, component, error::ErrorBoundary, - prelude::{CollectView, ElementChild, Get, GetUntracked}, + prelude::{ClassAttribute, CollectView, ElementChild, Get, GetUntracked}, view, }; use leptos_router::{ NavigateOptions, hooks::{use_navigate, use_query_map}, }; -use rocie_client::apis::configuration::Configuration; -use thaw::{Layout, LayoutPosition}; -use crate::components::{product_overview::ProductOverview, side_header::SiteHeader}; +use crate::components::{ + product_overview::ProductOverview, recipies::Recipies, site_header::SiteHeader, +}; #[component] -pub fn Home(config: Arc<Configuration>) -> impl IntoView { +pub fn Home() -> impl IntoView { let query_map = use_query_map().get_untracked(); let navigate = use_navigate(); - // mobile page if let Some(path) = query_map.get("path") { navigate(&path, NavigateOptions::default()); } @@ -44,12 +41,11 @@ pub fn Home(config: Arc<Configuration>) -> impl IntoView { } }> - <Layout position=LayoutPosition::Absolute> - <SiteHeader /> - <Layout> - <ProductOverview config /> - </Layout> - </Layout> + <div class="flex flex-col content-start"> + <SiteHeader logo=icondata_io::IoRoseSharp back_location="/" name="Rocie" /> + <ProductOverview /> + <Recipies /> + </div> </ErrorBoundary> } } |
