From 5296ed10c17fa9604d1709075168933cc6aac800 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 12 Feb 2022 12:11:36 -0500 Subject: [PATCH] MealPlan: Show shopping list and recipes --- web/src/components/header.rs | 4 ++-- web/src/components/recipe.rs | 3 +-- web/src/components/root.rs | 10 ++++----- web/src/components/shopping.rs | 41 +++++++++++++++++++++++++--------- web/src/service.rs | 30 +++++++++++++++++++------ web/src/web.rs | 8 +++---- 6 files changed, 66 insertions(+), 30 deletions(-) diff --git a/web/src/components/header.rs b/web/src/components/header.rs index 3114c31..fcc7a08 100644 --- a/web/src/components/header.rs +++ b/web/src/components/header.rs @@ -18,9 +18,9 @@ use sycamore::prelude::*; pub fn header() -> View { view! { div(class="menu") { - span { a(href="/ui/") { "home" }} + span { a(href="/ui/") { "Home" }} " | " - span { a(href="/ui/shopping/") { "shopping list" }} + span { a(href="/ui/plan/") { "Meal Plan" }} } } } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 556cfc8..253a1e0 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -48,8 +48,7 @@ fn steps(steps: ReadSignal>) -> View { #[component(Recipe)] pub fn recipe(idx: ReadSignal) -> View { let app_service = use_context::(); - // TODO(jwall): This does unnecessary copies. Can we eliminate that? - let recipe = create_memo(move || app_service.get_recipes().get()[*idx.get()].1.clone()); + let recipe = app_service.get_recipes().get()[*idx.get()].1.clone(); let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone())); let desc = create_memo( cloned!((recipe) => move || recipe.clone().get().desc.clone().unwrap_or_else(|| String::new())), diff --git a/web/src/components/root.rs b/web/src/components/root.rs index 839c731..59cd26c 100644 --- a/web/src/components/root.rs +++ b/web/src/components/root.rs @@ -14,6 +14,7 @@ use crate::components::*; use crate::service::AppService; +use recipes; use sycamore::{context::use_context, prelude::*}; #[component(Start)] @@ -40,16 +41,15 @@ pub fn recipe_list() -> View { let app_service = use_context::(); let titles = create_memo(cloned!(app_service => move || { - app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.title.clone())).collect::>() + app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.clone())).collect::)>>() })); view! { ul(class="recipe_list") { - Keyed(KeyedProps{ + Indexed(IndexedProps{ iterable: titles, - template: |(i, title)| { - view! { li { a(href=format!("/ui/recipe/{}", i)) { (title) } } } + template: |(i, recipe)| { + view! { li { a(href=format!("/ui/recipe/{}", i)) { (recipe.get().title) } } } }, - key: |(i, title)| (*i, title.clone()), }) } } diff --git a/web/src/components/shopping.rs b/web/src/components/shopping.rs index 4811aad..6fd66e9 100644 --- a/web/src/components/shopping.rs +++ b/web/src/components/shopping.rs @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::components::Recipe; use crate::console_log; use crate::service::AppService; use std::rc::Rc; @@ -20,7 +21,7 @@ use sycamore::{context::use_context, prelude::*}; struct RecipeCheckBoxProps { i: usize, - title: String, + title: ReadSignal, } #[component(RecipeSelection)] @@ -38,7 +39,7 @@ fn recipe_selection(props: RecipeCheckBoxProps) -> View { console_log!("setting recipe id: {} to count: {}", i, *count.get()); app_service.set_recipe_count_by_index(i, count.get().parse().unwrap()); }) - label(for=id_cloned_2) { (props.title) } + label(for=id_cloned_2) { (props.title.get()) } } } @@ -46,18 +47,17 @@ fn recipe_selection(props: RecipeCheckBoxProps) -> View { pub fn recipe_selector() -> View { let app_service = use_context::(); let titles = create_memo(cloned!(app_service => move || { - app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.title.clone())).collect::>() + app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.clone())).collect::)>>() })); view! { fieldset(class="recipe_selector") { - Keyed(KeyedProps{ + Indexed(IndexedProps{ iterable: titles, - template: |(i, title)| { + template: |(i, recipe)| { view! { - RecipeSelection(RecipeCheckBoxProps{i: i, title: title}) + RecipeSelection(RecipeCheckBoxProps{i: i, title: create_memo(move || recipe.get().title.clone())}) } }, - key: |(i, title)| (*i, title.clone()), }) } } @@ -67,7 +67,7 @@ pub fn recipe_selector() -> View { fn shopping_list() -> View { let app_service = use_context::(); let ingredients = create_memo(move || { - let ingredients = app_service.get_menu_list(); + let ingredients = app_service.get_shopping_list(); ingredients .iter() .map(|(k, v)| (k.clone(), v.clone())) @@ -76,6 +76,7 @@ fn shopping_list() -> View { // TODO(jwall): Sort by categories and names. view! { + h1 { "Shopping List" } table(class="shopping_list") { tr { th { "Quantity" } @@ -97,13 +98,33 @@ fn shopping_list() -> View { } } -#[component(ShoppingView)] -pub fn shopping_view() -> View { +#[component(RecipeList)] +fn recipe_list() -> View { + let app_service = use_context::(); + let menu_list = app_service.get_menu_list(); + view! { + h1 { "Recipe List" } + Indexed(IndexedProps{ + iterable: menu_list, + template: |(idx, _count)| { + let idx = Signal::new(idx); + view ! { + Recipe(idx.handle()) + hr() + } + } + }) + } +} + +#[component(MealPlan)] +pub fn meal_plan() -> View { view! { h1 { "Select your recipes" } RecipeSelector() ShoppingList() + RecipeList() } } diff --git a/web/src/service.rs b/web/src/service.rs index 0986e8f..13b1f88 100644 --- a/web/src/service.rs +++ b/web/src/service.rs @@ -23,7 +23,7 @@ use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe}; #[derive(Clone)] pub struct AppService { // TODO(jwall): Should each Recipe also be a Signal? - recipes: Signal>, + recipes: Signal)>>, menu_list: Signal>, } @@ -64,16 +64,16 @@ impl AppService { } } - pub fn get_recipe_by_index(&self, idx: usize) -> Option { + pub fn get_recipe_by_index(&self, idx: usize) -> Option> { self.recipes.get().get(idx).map(|(_, r)| r.clone()) } - pub fn get_menu_list(&self) -> BTreeMap { + pub fn get_shopping_list(&self) -> BTreeMap { let mut acc = IngredientAccumulator::new(); let recipe_counts = self.menu_list.get(); for (idx, count) in recipe_counts.iter() { for _ in 0..*count { - acc.accumulate_from(&self.get_recipe_by_index(*idx).unwrap()); + acc.accumulate_from(self.get_recipe_by_index(*idx).unwrap().get().as_ref()); } } acc.ingredients() @@ -89,11 +89,27 @@ impl AppService { self.menu_list.get().get(&i).map(|i| *i).unwrap_or_default() } - pub fn get_recipes(&self) -> Signal> { + pub fn get_recipes(&self) -> Signal)>> { self.recipes.clone() } - pub fn set_recipes(&mut self, recipes: Vec<(usize, Recipe)>) { - self.recipes.set(recipes); + pub fn get_menu_list(&self) -> ReadSignal> { + let menu_list = self.menu_list.clone(); + create_memo(move || { + menu_list + .get() + .iter() + .map(|(idx, count)| (*idx, *count)) + .collect::>() + }) + } + + pub fn set_recipes(&mut self, mut recipes: Vec<(usize, Recipe)>) { + self.recipes.set( + recipes + .drain(0..) + .map(|(i, r)| (i, Signal::new(r))) + .collect(), + ); } } diff --git a/web/src/web.rs b/web/src/web.rs index 1ab96fa..11311f8 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -27,8 +27,8 @@ enum AppRoutes { Root, #[to("/ui/recipe/")] Recipe { index: usize }, - #[to("/ui/shopping")] - Menu, + #[to("/ui/plan")] + Plan, #[not_found] NotFound, } @@ -42,8 +42,8 @@ fn route_switch(route: ReadSignal) -> View { AppRoutes::Recipe { index: idx } => view! { RecipeView(*idx) }, - AppRoutes::Menu => view! { - ShoppingView() + AppRoutes::Plan => view! { + MealPlan() }, AppRoutes::NotFound => view! { "NotFound"