From d0263ce46160cd4152c67381fab2ee557f3aa483 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Tue, 30 Sep 2025 09:15:56 +0200 Subject: feat(treewide): Switch to tailwindcss and add more components --- src/api/mod.rs | 50 +++++++++++++++++++++++++++ src/components/async_fetch.rs | 50 +++++++++++++++++++++++++++ src/components/container.rs | 47 +++++++++++++------------ src/components/icon_p.rs | 18 ++++++++++ src/components/inventory.rs | 55 +++++++++++++++++++++++++++++ src/components/mod.rs | 6 +++- src/components/product_overview.rs | 71 +++++++++++++++++--------------------- src/components/recipies.rs | 12 +++++++ src/components/side_header.rs | 22 ------------ src/components/site_header.rs | 29 ++++++++++++++++ src/lib.rs | 41 ++++++++++++++++------ src/main.rs | 5 +++ src/pages/home.rs | 24 ++++++------- 13 files changed, 321 insertions(+), 109 deletions(-) create mode 100644 src/api/mod.rs create mode 100644 src/components/async_fetch.rs create mode 100644 src/components/icon_p.rs create mode 100644 src/components/inventory.rs create mode 100644 src/components/recipies.rs delete mode 100644 src/components/side_header.rs create mode 100644 src/components/site_header.rs (limited to 'src') 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 { + let config = expect_context::>(); + amount_by_id(&config.config().read(), product_id) + .await + .map_err(Into::::into) +} +pub(crate) async fn get_product_by_id(product_id: ProductId) -> Result { + let config = expect_context::>(); + product_by_id(&config.config().read(), product_id) + .await + .map_err(Into::::into) +} +pub(crate) async fn get_unit_by_id(unit_id: UnitId) -> Result { + let config = expect_context::>(); + unit_by_id(&config.config().read(), unit_id) + .await + .map_err(Into::::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, Error> { + let config = expect_context::>(); + products(&config.config().read()) + .await + .map_err(Into::::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! { + "Loading..."

} + }> + {move || Suspend::new(async move { + let resource = { LocalResource::new(move || $fetcher) }; + resource + .await + .map(|$bound_variable| $producer) + })} +
+ } + }}; +} +pub(crate) use AsyncFetch; + +// #[component] +// pub fn AsyncFetch( +// fetcher: impl Fn() -> Fut + 'static + Send + Sync, +// producer: P, +// ) -> impl IntoView +// where +// V: IntoView + 'static, +// P: Fn(T) -> V + 'static + Send + Sync, +// Fut: Future> + 'static, +// T: 'static, +// LocalResource>: IntoFuture> + Send, +// { +// view! { +// "Loading..."

} +// }> +// { || Suspend::new(async { +// let value_resource = LocalResource::new( || fetcher()); +// value_resource +// .await +// .map(|value| { +// producer(value) +// }) +// })} +//
+// } +// } 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! { - - -
-

{header}

+
+

{header}

{children()} + +
    + {buttons + .into_iter() + .map(|(name, path)| { + view! { +
  • + +
  • + } + }) + .collect::>()} +
} } 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, text: &'static str) -> impl IntoView { + view! { +
+
+ +
+

{text}

+
+ } +} 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! { + + +
    + { + 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! { +
      +
    • {product.name}
    • +
    • + + { + format!( + "{} {}", + amount.amount.value, + unit.short_name + ) + } + +
    • +
    + } + } + }} + } + }) + .collect::>() + } + } + } +
+ } +} 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) -> 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! { - -

{read_status}

+ }, "inventory"), + (view! { }, "consume"), + (view! { }, "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! { +

+ {format!( + "You have {products_num} product{plural_s} \ + in stock with a value \ + of {products_value} {products_currency}.", + )} +

+ } + } + )}
} } 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! { + +

"You have 0 recipies."

+
+ } +} 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! { - - - -

"Rocie"

-
-
- } -} 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, +) -> impl IntoView { + let navigate = use_navigate(); + + view! { +
+
+ +
+

{name}

+
{menu}
+
+ } +} 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! { - + <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> } } -- cgit 1.4.1