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> {
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" }}
}
}
}

View File

@ -48,8 +48,7 @@ fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
#[component(Recipe<G>)]
pub fn recipe(idx: ReadSignal<usize>) -> View<G> {
let app_service = use_context::<AppService>();
// 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())),

View File

@ -14,6 +14,7 @@
use crate::components::*;
use crate::service::AppService;
use recipes;
use sycamore::{context::use_context, prelude::*};
#[component(Start<G>)]
@ -40,16 +41,15 @@ pub fn recipe_list() -> View<G> {
let app_service = use_context::<AppService>();
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! {
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()),
})
}
}

View File

@ -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<String>,
}
#[component(RecipeSelection<G>)]
@ -38,7 +39,7 @@ fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> {
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<G> {
pub fn recipe_selector() -> View<G> {
let app_service = use_context::<AppService>();
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! {
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<G> {
fn shopping_list() -> View<G> {
let app_service = use_context::<AppService>();
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<G> {
// 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<G> {
}
}
#[component(ShoppingView<G>)]
pub fn shopping_view() -> View<G> {
#[component(RecipeList<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! {
h1 {
"Select your recipes"
}
RecipeSelector()
ShoppingList()
RecipeList()
}
}

View File

@ -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<Vec<(usize, Recipe)>>,
recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
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())
}
pub fn get_menu_list(&self) -> BTreeMap<IngredientKey, Ingredient> {
pub fn get_shopping_list(&self) -> BTreeMap<IngredientKey, Ingredient> {
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<Vec<(usize, Recipe)>> {
pub fn get_recipes(&self) -> Signal<Vec<(usize, Signal<Recipe>)>> {
self.recipes.clone()
}
pub fn set_recipes(&mut self, recipes: Vec<(usize, Recipe)>) {
self.recipes.set(recipes);
pub fn get_menu_list(&self) -> ReadSignal<Vec<(usize, usize)>> {
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,
#[to("/ui/recipe/<index>")]
Recipe { index: usize },
#[to("/ui/shopping")]
Menu,
#[to("/ui/plan")]
Plan,
#[not_found]
NotFound,
}
@ -42,8 +42,8 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
AppRoutes::Recipe { index: idx } => view! {
RecipeView(*idx)
},
AppRoutes::Menu => view! {
ShoppingView()
AppRoutes::Plan => view! {
MealPlan()
},
AppRoutes::NotFound => view! {
"NotFound"