summary refs log tree commit diff stats
path: root/src/components
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-03-19 07:45:14 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-03-19 07:45:14 +0100
commitf6a3fb9c4d8dd86f78c9f75a23c1ac35bf35d4eb (patch)
tree5f28fbca03d83921b568f7cb1708374456d9ec42 /src/components
parentfeat(treewide): Add further buttons (diff)
downloadweb-client-f6a3fb9c4d8dd86f78c9f75a23c1ac35bf35d4eb.zip
feat(treewide): Commit MVP
Diffstat (limited to 'src/components')
-rw-r--r--src/components/async_fetch.rs12
-rw-r--r--src/components/buy.rs1
-rw-r--r--src/components/catch_errors.rs40
-rw-r--r--src/components/checkbox_placeholder.rs54
-rw-r--r--src/components/container.rs52
-rw-r--r--src/components/inventory.rs4
-rw-r--r--src/components/login_wall.rs42
-rw-r--r--src/components/mod.rs7
-rw-r--r--src/components/product_overview.rs19
-rw-r--r--src/components/product_parent_overview.rs37
-rw-r--r--src/components/recipies.rs20
-rw-r--r--src/components/textarea_placeholder.rs60
-rw-r--r--src/components/unit_overview.rs37
13 files changed, 336 insertions, 49 deletions
diff --git a/src/components/async_fetch.rs b/src/components/async_fetch.rs
index 7bf44a0..43469a7 100644
--- a/src/components/async_fetch.rs
+++ b/src/components/async_fetch.rs
@@ -37,11 +37,13 @@ macro_rules! AsyncFetch {
             <Transition fallback=|| {
                 view! { <p>"Loading..."</p> }
             }>
-                {move || Suspend::new(async move {
-                    $resource
-                        .await
-                        .map($producer)
-                })}
+                {
+                    Suspend::new(async move {
+                        $resource
+                            .await
+                            .map($producer)
+                    })
+                }
             </Transition>
         }
     }};
diff --git a/src/components/buy.rs b/src/components/buy.rs
index e69de29..8b13789 100644
--- a/src/components/buy.rs
+++ b/src/components/buy.rs
@@ -0,0 +1 @@
+
diff --git a/src/components/catch_errors.rs b/src/components/catch_errors.rs
new file mode 100644
index 0000000..d5a452d
--- /dev/null
+++ b/src/components/catch_errors.rs
@@ -0,0 +1,40 @@
+use leptos::{
+    IntoView, component,
+    error::ErrorBoundary,
+    prelude::{Children, ClassAttribute, CollectView, ElementChild, Get},
+    view,
+};
+
+use crate::components::site_header::SiteHeader;
+
+#[component]
+pub(crate) fn CatchErrors(children: Children) -> impl IntoView {
+    view! {
+        <ErrorBoundary fallback=|errors| {
+            view! {
+                <SiteHeader
+                    logo=icondata_io::IoRoseSharp
+                    back_location="/"
+                    name="Errors occurred"
+                />
+
+                <h1>"Uh oh! Something went wrong!"</h1>
+
+                <p>"Errors: "</p>
+                <ul class="flex flex-col gap-1">
+                    {move || {
+                        errors
+                            .get()
+                            .into_iter()
+                            .map(|(_, e)| {
+                                view! {
+                                    <li class="bg-gray-200 rounded-lg m-2 p-1">{e.to_string()}</li>
+                                }
+                            })
+                            .collect_view()
+                    }}
+                </ul>
+            }
+        }>{children()}</ErrorBoundary>
+    }
+}
diff --git a/src/components/checkbox_placeholder.rs b/src/components/checkbox_placeholder.rs
new file mode 100644
index 0000000..a1aaa0c
--- /dev/null
+++ b/src/components/checkbox_placeholder.rs
@@ -0,0 +1,54 @@
+use leptos::{
+    IntoView, component,
+    html::Input,
+    prelude::{ClassAttribute, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute},
+    view,
+};
+
+use crate::components::get_id;
+
+#[component]
+pub fn CheckboxPlaceholder(
+    label: &'static str,
+    node_ref: NodeRef<Input>,
+) -> impl IntoView {
+    let id = get_id();
+
+    view! {
+        <div class="relative h-14">
+            <input
+                id=id.to_string()
+                type="checkbox"
+                autocomplete="off"
+                class="\
+                absolute \
+                bottom-0 \
+                right-5 \
+                bg-gray-200 \
+                border-8 \
+                border-b-2 \
+                border-b-trasparent \
+                border-gray-200 \
+                focus:outline-none \
+                h-10 \
+                rounded-t-lg \
+                text-gray-900 \
+                "
+                node_ref=node_ref
+            />
+
+            <label
+                for=id.to_string()
+                class="\
+                bottom-10 \
+                absolute \
+                left-0 \
+                text-gray-700 \
+                text-sm \
+                "
+            >
+                {label}
+            </label>
+        </div>
+    }
+}
diff --git a/src/components/container.rs b/src/components/container.rs
index d6d2f03..3b56713 100644
--- a/src/components/container.rs
+++ b/src/components/container.rs
@@ -1,46 +1,38 @@
 use leptos::{
     IntoView, component,
-    prelude::{Children, ClassAttribute, ElementChild, OnAttribute},
+    prelude::{Children, ClassAttribute, ElementChild},
     view,
 };
-use leptos_router::{NavigateOptions, hooks::use_navigate};
+use leptos_router::components::A;
 
 #[component]
 pub fn Container(
-    header: impl IntoView,
-    buttons: Vec<(impl IntoView, &'static str)>,
+    header: impl IntoView + 'static,
+    buttons: Vec<(impl IntoView + 'static, &'static str)>,
     children: Children,
 ) -> impl IntoView {
     assert!(!buttons.is_empty());
 
-    let first_button_path = buttons.first().expect("Should have at least on button").1;
+    // TODO: Add the direct link to the first button back. <2026-02-15>
+    // let first_button_path = buttons.first().expect("Should have at least on button").1;
 
     view! {
-        <button
-            type="button"
-            on:click=|_| {
-                use_navigate()(first_button_path, NavigateOptions::default());
-            }
-        >
-            <div class="p-4 mt-4 mr-4 ml-4 md-2 text-justify rounded-lg border-gray-600 border">
-                <p class="text-lg text-bold">{header}</p>
-                {children()}
+        <div class="p-4 mt-4 mr-4 ml-4 md-2 text-justify rounded-lg border-gray-600 border">
+            <h2 class="text-lg text-bold">{header}</h2>
+            {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 text-nowrap first:rounded-l-full last:rounded-r-full">
-                                    <button on:click=move |_| {
-                                        use_navigate()(path, NavigateOptions::default());
-                                    }>{name}</button>
-                                </li>
-                            }
-                        })
-                        .collect::<Vec<_>>()}
-                </ul>
-            </div>
-        </button>
+            <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 text-nowrap first:rounded-l-full last:rounded-r-full">
+                                <A href=move || path.to_owned()>{name}</A>
+                            </li>
+                        }
+                    })
+                    .collect::<Vec<_>>()}
+            </ul>
+        </div>
     }
 }
diff --git a/src/components/inventory.rs b/src/components/inventory.rs
index 275dd0b..31b1c12 100644
--- a/src/components/inventory.rs
+++ b/src/components/inventory.rs
@@ -23,8 +23,8 @@ pub fn Inventory() -> impl IntoView {
                     producer = |products| {
                         let products_num = products.len();
                         let plural_s = if products_num == 1 { "" } else { "s" };
-                        let products_value = 2;
-                        let products_currency = "EUR";
+                        let products_value = -1;
+                        let products_currency = "TODO";
 
                         view! {
                             <p>
diff --git a/src/components/login_wall.rs b/src/components/login_wall.rs
new file mode 100644
index 0000000..fd5c64f
--- /dev/null
+++ b/src/components/login_wall.rs
@@ -0,0 +1,42 @@
+use leptos::{
+    IntoView, component,
+    error::Error,
+    prelude::{Children, IntoAny},
+    view,
+};
+use leptos_router::{NavigateOptions, hooks::use_navigate};
+
+use crate::{
+    api::{can_be_provisioned_wrapped, is_logged_in_wrapped},
+    components::async_fetch::{AsyncFetch, AsyncResource},
+};
+
+#[component]
+pub fn LoginWall(
+    back: impl Fn() -> String + Send + Sync + 'static,
+    children: Children,
+) -> impl IntoView {
+    view! {
+        {
+            AsyncFetch! {
+                @map_error_in_producer
+                from_resource = AsyncResource!(
+                    () -> Result<(bool, bool), Error> {
+                        Ok((can_be_provisioned_wrapped().await?, is_logged_in_wrapped().await?))
+                    }
+                ),
+                producer = |(can_be_provisioned, is_logged_in)| {
+                    if is_logged_in {
+                        children()
+                    } else if can_be_provisioned {
+                        use_navigate()(format!("/provision/?back={}", back()).as_str(), NavigateOptions::default());
+                        ().into_any()
+                    } else {
+                        use_navigate()(format!("/login/?back={}", back()).as_str(), NavigateOptions::default());
+                        ().into_any()
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 2c3d79a..2a3a0b1 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -3,15 +3,22 @@ use std::sync::atomic::{AtomicU32, Ordering};
 // Generic
 pub mod async_fetch;
 pub mod banner;
+pub mod catch_errors;
 pub mod container;
 pub mod form;
 pub mod icon_p;
+pub mod login_wall;
+
+// placeholders
+pub mod checkbox_placeholder;
 pub mod input_placeholder;
 pub mod select_placeholder;
+pub mod textarea_placeholder;
 
 // Specific
 pub mod inventory;
 pub mod product_overview;
+pub mod product_parent_overview;
 pub mod recipies;
 pub mod site_header;
 pub mod unit_overview;
diff --git a/src/components/product_overview.rs b/src/components/product_overview.rs
index bf81624..233b8a7 100644
--- a/src/components/product_overview.rs
+++ b/src/components/product_overview.rs
@@ -11,9 +11,20 @@ pub fn ProductOverview() -> impl IntoView {
         <Container
             header="Products"
             buttons=vec![
-                (view! { <IconP icon=icondata_io::IoClipboard text="Show products" /> }, "products"),
-                (view! { <IconP icon=icondata_io::IoPricetags text="Create product" /> }, "create-product"),
-                (view! { <IconP icon=icondata_io::IoPricetags text="Associate barcode with product" /> }, "associate-barcode-product"),
+                (view! { <IconP icon=icondata_io::IoClipboard text="Show" /> }, "products"),
+                (
+                    view! { <IconP icon=icondata_io::IoPricetags text="Create product" /> },
+                    "create-product",
+                ),
+                (
+                    view! {
+                        <IconP
+                            icon=icondata_io::IoPricetags
+                            text="Associate barcode with product"
+                        />
+                    },
+                    "associate-barcode-product",
+                ),
             ]
         >
             {
@@ -22,7 +33,7 @@ pub fn ProductOverview() -> impl IntoView {
                     fetcher = products_registered_wrapped(),
                     producer = |products| {
                         view! {
-                            <p>{format!("You have {} products", products.len())}</p>
+                            <p>{format!("You have {} products.", products.len())}</p>
                         }
                     }
                 }
diff --git a/src/components/product_parent_overview.rs b/src/components/product_parent_overview.rs
new file mode 100644
index 0000000..4aa2a0f
--- /dev/null
+++ b/src/components/product_parent_overview.rs
@@ -0,0 +1,37 @@
+use leptos::{IntoView, component, view};
+
+use crate::{
+    api::product_parents_wrapped,
+    components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP},
+};
+
+#[component]
+pub fn ProductParentOverview() -> impl IntoView {
+    view! {
+        <Container
+            header="Products Parents"
+            buttons=vec![
+                (
+                    view! { <IconP icon=icondata_io::IoClipboard text="Show products" /> },
+                    "products",
+                ),
+                (
+                    view! { <IconP icon=icondata_io::IoPricetags text="Create product parent" /> },
+                    "create-product-parent",
+                ),
+            ]
+        >
+            {
+                AsyncFetch! {
+                    @map_error_in_producer
+                    fetcher = product_parents_wrapped(),
+                    producer = |product_parents| {
+                        view! {
+                            <p>{format!("You have {} product parents.", product_parents.len())}</p>
+                        }
+                    }
+                }
+            }
+        </Container>
+    }
+}
diff --git a/src/components/recipies.rs b/src/components/recipies.rs
index f7903e4..755954e 100644
--- a/src/components/recipies.rs
+++ b/src/components/recipies.rs
@@ -1,6 +1,9 @@
-use leptos::{IntoView, component, prelude::ElementChild, view};
+use leptos::{IntoView, component, view};
 
-use crate::components::{container::Container, icon_p::IconP};
+use crate::{
+    api::recipes_wrapped,
+    components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP},
+};
 
 #[component]
 pub fn Recipies() -> impl IntoView {
@@ -9,10 +12,21 @@ pub fn Recipies() -> impl IntoView {
             header="Recipies"
             buttons=vec![
                 (view! { <IconP icon=icondata_io::IoFastFood text="Recipies" /> }, "recipies"),
+                (view! { <IconP icon=icondata_io::IoPin text="Create recipe" /> }, "create-recipe"),
                 (view! { <IconP icon=icondata_io::IoCalendarSharp text="Mealplan" /> }, "mealplan"),
             ]
         >
-            <p>"You have 0 recipies."</p>
+            {
+                AsyncFetch! {
+                    @map_error_in_producer
+                    fetcher = recipes_wrapped(),
+                    producer = |recipes| {
+                        view! {
+                            <p>{format!("You have {} recipies.", recipes.len())}</p>
+                        }
+                    }
+                }
+            }
         </Container>
     }
 }
diff --git a/src/components/textarea_placeholder.rs b/src/components/textarea_placeholder.rs
new file mode 100644
index 0000000..a0bae6d
--- /dev/null
+++ b/src/components/textarea_placeholder.rs
@@ -0,0 +1,60 @@
+use leptos::{
+    IntoView, component,
+    html::Textarea,
+    prelude::{ClassAttribute, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute},
+    view,
+};
+
+use crate::components::get_id;
+
+#[component]
+pub fn TextareaPlaceholder(
+    label: &'static str,
+    node_ref: NodeRef<Textarea>,
+    #[prop(default = None)] initial_value: Option<String>,
+) -> impl IntoView {
+    let id = get_id();
+
+    view! {
+        <div class="relative h-80">
+            <textarea
+                id=id.to_string()
+                class="\
+                absolute \
+                bottom-0 \
+                bg-gray-200 \
+                border-2 \
+                border-b-2 \
+                border-b-trasparent \
+                border-gray-200 \
+                focus:border-indigo-600 \
+                focus:border-b-transparent \
+                focus:outline-none \
+                h-[300px] \
+                placeholder-transparent \
+                rounded-t-lg \
+                text-gray-900 \
+                w-full \
+                "
+                placeholder="sentinel value"
+                node_ref=node_ref
+            >
+                {initial_value}
+            </textarea>
+
+            <label
+                for=id.to_string()
+                class="\
+                absolute \
+                transition-all \
+                text-sm \
+                text-gray-700 \
+                top-0 \
+                left-0 \
+                "
+            >
+                {label}
+            </label>
+        </div>
+    }
+}
diff --git a/src/components/unit_overview.rs b/src/components/unit_overview.rs
index 25e5675..0ea3825 100644
--- a/src/components/unit_overview.rs
+++ b/src/components/unit_overview.rs
@@ -1,6 +1,10 @@
 use leptos::{IntoView, component, view};
+use rocie_client::models::{Unit, UnitProperty};
 
-use crate::components::{container::Container, icon_p::IconP};
+use crate::{
+    api::{unit_properties_wrapped, units_wrapped},
+    components::{async_fetch::AsyncFetch, container::Container, icon_p::IconP},
+};
 
 #[component]
 pub fn UnitOverview() -> impl IntoView {
@@ -8,14 +12,37 @@ pub fn UnitOverview() -> impl IntoView {
         <Container
             header="Units"
             buttons=vec![
-                (view! { <IconP icon=icondata_io::IoClipboard text="Show unit" /> }, "units"),
-                (view! { <IconP icon=icondata_io::IoClipboard text="Create unit" /> }, "create-unit"),
-                (view! { <IconP icon=icondata_io::IoPricetags text="Create unit property" /> }, "create-unit-property"),
+                (view! { <IconP icon=icondata_io::IoClipboard text="Show" /> }, "units"),
+                (
+                    view! { <IconP icon=icondata_io::IoClipboard text="Create unit" /> },
+                    "create-unit",
+                ),
+                (
+                    view! { <IconP icon=icondata_io::IoPricetags text="Create unit property" /> },
+                    "create-unit-property",
+                ),
             ]
         >
             {
-                "You have units"
+                AsyncFetch! {
+                    @map_error_in_producer
+                    fetcher = get_units_and_unit_properties(),
+                    producer = |(units, unit_properties)| {
+                        view! {
+                            <p>{move || format!(
+                                "You have {} units and {} unit properties.",
+                                units.len(),
+                                unit_properties.len()
+                            )}</p>
+                        }
+                    },
+                }
             }
         </Container>
     }
 }
+
+async fn get_units_and_unit_properties()
+-> Result<(Vec<Unit>, Vec<UnitProperty>), leptos::error::Error> {
+    Ok((units_wrapped().await?, unit_properties_wrapped().await?))
+}