MealPlan: Show shopping list and recipes

This commit is contained in:
Jeremy Wall 2022-02-12 12:11:36 -05:00
parent 9c5d127b4e
commit 5296ed10c1
6 changed files with 66 additions and 30 deletions

View File

@ -18,9 +18,9 @@ use sycamore::prelude::*;
pub fn header() -> View<G> { pub fn header() -> View<G> {
view! { view! {
div(class="menu") { 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" }}
} }
} }
} }

View File

@ -48,8 +48,7 @@ fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
#[component(Recipe<G>)] #[component(Recipe<G>)]
pub fn recipe(idx: ReadSignal<usize>) -> View<G> { pub fn recipe(idx: ReadSignal<usize>) -> View<G> {
let app_service = use_context::<AppService>(); let app_service = use_context::<AppService>();
// TODO(jwall): This does unnecessary copies. Can we eliminate that? let recipe = app_service.get_recipes().get()[*idx.get()].1.clone();
let recipe = create_memo(move || app_service.get_recipes().get()[*idx.get()].1.clone());
let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone())); let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone()));
let desc = create_memo( let desc = create_memo(
cloned!((recipe) => move || recipe.clone().get().desc.clone().unwrap_or_else(|| String::new())), cloned!((recipe) => move || recipe.clone().get().desc.clone().unwrap_or_else(|| String::new())),

View File

@ -14,6 +14,7 @@
use crate::components::*; use crate::components::*;
use crate::service::AppService; use crate::service::AppService;
use recipes;
use sycamore::{context::use_context, prelude::*}; use sycamore::{context::use_context, prelude::*};
#[component(Start<G>)] #[component(Start<G>)]
@ -40,16 +41,15 @@ pub fn recipe_list() -> View<G> {
let app_service = use_context::<AppService>(); let app_service = use_context::<AppService>();
let titles = create_memo(cloned!(app_service => move || { let titles = create_memo(cloned!(app_service => move || {
app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.title.clone())).collect::<Vec<(usize, String)>>() app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.clone())).collect::<Vec<(usize, Signal<recipes::Recipe>)>>()
})); }));
view! { view! {
ul(class="recipe_list") { ul(class="recipe_list") {
Keyed(KeyedProps{ Indexed(IndexedProps{
iterable: titles, iterable: titles,
template: |(i, title)| { template: |(i, recipe)| {
view! { li { a(href=format!("/ui/recipe/{}", i)) { (title) } } } view! { li { a(href=format!("/ui/recipe/{}", i)) { (recipe.get().title) } } }
}, },
key: |(i, title)| (*i, title.clone()),
}) })
} }
} }

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::components::Recipe;
use crate::console_log; use crate::console_log;
use crate::service::AppService; use crate::service::AppService;
use std::rc::Rc; use std::rc::Rc;
@ -20,7 +21,7 @@ use sycamore::{context::use_context, prelude::*};
struct RecipeCheckBoxProps { struct RecipeCheckBoxProps {
i: usize, i: usize,
title: String, title: ReadSignal<String>,
} }
#[component(RecipeSelection<G>)] #[component(RecipeSelection<G>)]
@ -38,7 +39,7 @@ fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> {
console_log!("setting recipe id: {} to count: {}", i, *count.get()); console_log!("setting recipe id: {} to count: {}", i, *count.get());
app_service.set_recipe_count_by_index(i, count.get().parse().unwrap()); 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<G> {
pub fn recipe_selector() -> View<G> { pub fn recipe_selector() -> View<G> {
let app_service = use_context::<AppService>(); let app_service = use_context::<AppService>();
let titles = create_memo(cloned!(app_service => move || { let titles = create_memo(cloned!(app_service => move || {
app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.title.clone())).collect::<Vec<(usize, String)>>() app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.clone())).collect::<Vec<(usize, Signal<recipes::Recipe>)>>()
})); }));
view! { view! {
fieldset(class="recipe_selector") { fieldset(class="recipe_selector") {
Keyed(KeyedProps{ Indexed(IndexedProps{
iterable: titles, iterable: titles,
template: |(i, title)| { template: |(i, recipe)| {
view! { 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<G> {
fn shopping_list() -> View<G> { fn shopping_list() -> View<G> {
let app_service = use_context::<AppService>(); let app_service = use_context::<AppService>();
let ingredients = create_memo(move || { let ingredients = create_memo(move || {
let ingredients = app_service.get_menu_list(); let ingredients = app_service.get_shopping_list();
ingredients ingredients
.iter() .iter()
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
@ -76,6 +76,7 @@ fn shopping_list() -> View<G> {
// TODO(jwall): Sort by categories and names. // TODO(jwall): Sort by categories and names.
view! { view! {
h1 { "Shopping List" }
table(class="shopping_list") { table(class="shopping_list") {
tr { tr {
th { "Quantity" } th { "Quantity" }
@ -97,13 +98,33 @@ fn shopping_list() -> View<G> {
} }
} }
#[component(ShoppingView<G>)] #[component(RecipeList<G>)]
pub fn shopping_view() -> View<G> { fn recipe_list() -> View<G> {
let app_service = use_context::<AppService>();
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<G>)]
pub fn meal_plan() -> View<G> {
view! { view! {
h1 { h1 {
"Select your recipes" "Select your recipes"
} }
RecipeSelector() RecipeSelector()
ShoppingList() ShoppingList()
RecipeList()
} }
} }

View File

@ -23,7 +23,7 @@ use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe};
#[derive(Clone)] #[derive(Clone)]
pub struct AppService { pub struct AppService {
// TODO(jwall): Should each Recipe also be a Signal? // TODO(jwall): Should each Recipe also be a Signal?
recipes: Signal<Vec<(usize, Recipe)>>, recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
menu_list: Signal<BTreeMap<usize, usize>>, menu_list: Signal<BTreeMap<usize, usize>>,
} }
@ -64,16 +64,16 @@ impl AppService {
} }
} }
pub fn get_recipe_by_index(&self, idx: usize) -> Option<Recipe> { pub fn get_recipe_by_index(&self, idx: usize) -> Option<Signal<Recipe>> {
self.recipes.get().get(idx).map(|(_, r)| r.clone()) self.recipes.get().get(idx).map(|(_, r)| r.clone())
} }
pub fn get_menu_list(&self) -> BTreeMap<IngredientKey, Ingredient> { pub fn get_shopping_list(&self) -> BTreeMap<IngredientKey, Ingredient> {
let mut acc = IngredientAccumulator::new(); let mut acc = IngredientAccumulator::new();
let recipe_counts = self.menu_list.get(); let recipe_counts = self.menu_list.get();
for (idx, count) in recipe_counts.iter() { for (idx, count) in recipe_counts.iter() {
for _ in 0..*count { 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() acc.ingredients()
@ -89,11 +89,27 @@ impl AppService {
self.menu_list.get().get(&i).map(|i| *i).unwrap_or_default() self.menu_list.get().get(&i).map(|i| *i).unwrap_or_default()
} }
pub fn get_recipes(&self) -> Signal<Vec<(usize, Recipe)>> { pub fn get_recipes(&self) -> Signal<Vec<(usize, Signal<Recipe>)>> {
self.recipes.clone() self.recipes.clone()
} }
pub fn set_recipes(&mut self, recipes: Vec<(usize, Recipe)>) { pub fn get_menu_list(&self) -> ReadSignal<Vec<(usize, usize)>> {
self.recipes.set(recipes); let menu_list = self.menu_list.clone();
create_memo(move || {
menu_list
.get()
.iter()
.map(|(idx, count)| (*idx, *count))
.collect::<Vec<(usize, usize)>>()
})
}
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(),
);
} }
} }

View File

@ -27,8 +27,8 @@ enum AppRoutes {
Root, Root,
#[to("/ui/recipe/<index>")] #[to("/ui/recipe/<index>")]
Recipe { index: usize }, Recipe { index: usize },
#[to("/ui/shopping")] #[to("/ui/plan")]
Menu, Plan,
#[not_found] #[not_found]
NotFound, NotFound,
} }
@ -42,8 +42,8 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
AppRoutes::Recipe { index: idx } => view! { AppRoutes::Recipe { index: idx } => view! {
RecipeView(*idx) RecipeView(*idx)
}, },
AppRoutes::Menu => view! { AppRoutes::Plan => view! {
ShoppingView() MealPlan()
}, },
AppRoutes::NotFound => view! { AppRoutes::NotFound => view! {
"NotFound" "NotFound"